@mingxy/cerebro 1.15.13 → 1.16.0
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 -31
- package/src/client.ts +9 -1
- package/src/config.ts +5 -17
- package/src/hooks.ts +184 -817
- 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();
|
|
@@ -262,14 +243,30 @@ const FETCH_POLICY = [
|
|
|
262
243
|
"</cerebro-fetch-policy>",
|
|
263
244
|
].join("\n");
|
|
264
245
|
|
|
265
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Score-weighted budget allocation: high-score memories get more chars.
|
|
248
|
+
* Falls back to uniform distribution when totalScore === 0 or all scores equal.
|
|
249
|
+
*/
|
|
250
|
+
function buildContextBlock(
|
|
251
|
+
results: SearchResult[],
|
|
252
|
+
budget: number,
|
|
253
|
+
maxContentLength: number = 500,
|
|
254
|
+
minItemChars: number = MIN_ITEM_CONTENT_CHARS,
|
|
255
|
+
): string {
|
|
266
256
|
if (results.length === 0) return "";
|
|
267
257
|
|
|
258
|
+
const totalScore = results.reduce((sum, r) => sum + r.score, 0);
|
|
259
|
+
|
|
268
260
|
const grouped = categorize(results);
|
|
269
261
|
const sections: string[] = [];
|
|
270
262
|
|
|
271
263
|
for (const [label, items] of grouped) {
|
|
272
|
-
const lines = items.map((r) =>
|
|
264
|
+
const lines = items.map((r) => {
|
|
265
|
+
const itemMaxLen = totalScore > 0
|
|
266
|
+
? Math.min(maxContentLength, Math.max(minItemChars, Math.floor((r.score / totalScore) * budget)))
|
|
267
|
+
: Math.min(maxContentLength, Math.max(minItemChars, Math.floor(budget / results.length)));
|
|
268
|
+
return formatMemoryLine(r, itemMaxLen);
|
|
269
|
+
});
|
|
273
270
|
sections.push(`[${label}]\n${lines.join("\n")}`);
|
|
274
271
|
}
|
|
275
272
|
|
|
@@ -281,13 +278,23 @@ function buildContextBlock(results: SearchResult[], maxContentLength: number = 5
|
|
|
281
278
|
].join("\n");
|
|
282
279
|
}
|
|
283
280
|
|
|
284
|
-
function buildClusteredContextBlock(
|
|
281
|
+
function buildClusteredContextBlock(
|
|
282
|
+
clustered: import("./client.js").ClusteredRecallResult,
|
|
283
|
+
budget: number,
|
|
284
|
+
maxContentLength: number = 500,
|
|
285
|
+
minItemChars: number = MIN_ITEM_CONTENT_CHARS,
|
|
286
|
+
): string {
|
|
285
287
|
const sections: string[] = [];
|
|
286
288
|
|
|
287
289
|
if (clustered.cluster_summaries.length > 0) {
|
|
290
|
+
const totalClusterScore = clustered.cluster_summaries.reduce((sum, cs) => sum + cs.relevance_score, 0);
|
|
291
|
+
|
|
288
292
|
sections.push("## 📋 主题簇(聚合记忆)");
|
|
289
293
|
for (const cs of clustered.cluster_summaries) {
|
|
290
294
|
const scoreIndicator = cs.relevance_score >= 0.8 ? "★★★" : cs.relevance_score >= 0.6 ? "★★" : "★";
|
|
295
|
+
const clusterMaxLen = totalClusterScore > 0
|
|
296
|
+
? Math.min(maxContentLength, Math.max(minItemChars, Math.floor((cs.relevance_score / totalClusterScore) * budget)))
|
|
297
|
+
: Math.min(maxContentLength, Math.max(minItemChars, Math.floor(budget / clustered.cluster_summaries.length)));
|
|
291
298
|
sections.push(`\n### ${cs.title} (整合自${cs.member_count}条记忆) ${scoreIndicator}`);
|
|
292
299
|
sections.push(`> ${cs.summary}`);
|
|
293
300
|
if (cs.key_memories.length > 0) {
|
|
@@ -298,7 +305,7 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
|
|
|
298
305
|
? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
|
|
299
306
|
: "";
|
|
300
307
|
const importanceBar = mem.importance >= 0.7 ? "●" : mem.importance >= 0.4 ? "◐" : "○";
|
|
301
|
-
const content = truncate(mem.content,
|
|
308
|
+
const content = truncate(mem.content, clusterMaxLen);
|
|
302
309
|
sections.push(`- ${importanceBar}${idTag}${relTag} ${content}`);
|
|
303
310
|
}
|
|
304
311
|
}
|
|
@@ -306,13 +313,18 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
|
|
|
306
313
|
}
|
|
307
314
|
|
|
308
315
|
if (clustered.standalone_memories.length > 0) {
|
|
316
|
+
const standaloneBudget = clustered.cluster_summaries.length === 0
|
|
317
|
+
? budget
|
|
318
|
+
: Math.floor(budget * 0.3);
|
|
319
|
+
const standaloneMaxLen = Math.min(maxContentLength, Math.max(minItemChars, Math.floor(standaloneBudget / clustered.standalone_memories.length)));
|
|
320
|
+
|
|
309
321
|
sections.push("\n## 📌 补充信息");
|
|
310
322
|
for (const mem of clustered.standalone_memories) {
|
|
311
323
|
const idTag = mem.id ? ` [id:${mem.id}]` : "";
|
|
312
324
|
const relTag = mem.relations && mem.relations.length > 0
|
|
313
325
|
? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
|
|
314
326
|
: "";
|
|
315
|
-
const content = truncate(mem.content,
|
|
327
|
+
const content = truncate(mem.content, standaloneMaxLen);
|
|
316
328
|
sections.push(`-${idTag}${relTag} ${content}`);
|
|
317
329
|
}
|
|
318
330
|
}
|
|
@@ -335,7 +347,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
335
347
|
const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
|
|
336
348
|
const llmMaxEval = config.recall?.llmMaxEval ?? 15;
|
|
337
349
|
const refineStrategy = config.recall?.refineStrategy ?? "balanced";
|
|
338
|
-
const refineMediumChars = config.recall?.refineMediumChars ?? 200;
|
|
339
350
|
const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
|
|
340
351
|
const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
|
|
341
352
|
const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
|
|
@@ -352,44 +363,32 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
352
363
|
if (policy === "none") return;
|
|
353
364
|
|
|
354
365
|
try {
|
|
355
|
-
logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy
|
|
366
|
+
logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy });
|
|
356
367
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
357
368
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
358
369
|
|
|
359
|
-
// --- Profile Fetch (
|
|
360
|
-
const
|
|
370
|
+
// --- Profile Fetch (V2 inject API with TTL gate) ---
|
|
371
|
+
const profileTtlMs = config.profile?.ttlMs ?? 300000; // default 5 minutes
|
|
372
|
+
const lastInjected = profileInjectedSessions.get(input.sessionID);
|
|
373
|
+
const profileTtlExpired = !lastInjected || (Date.now() - lastInjected > profileTtlMs);
|
|
374
|
+
|
|
375
|
+
let profileBlock = "";
|
|
361
376
|
let profileInjected = false;
|
|
362
377
|
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 });
|
|
378
|
+
|
|
379
|
+
if (profileTtlExpired) {
|
|
380
|
+
try {
|
|
381
|
+
const injection = await client.getInjection(directory || process.env.OMEM_PROJECT_DIR);
|
|
382
|
+
if (injection?.content) {
|
|
383
|
+
profileBlock = injection.content;
|
|
384
|
+
profileCountText = `${injection.preference_count} preferences`;
|
|
385
|
+
profileInjected = true;
|
|
386
|
+
profileInjectedSessions.set(input.sessionID, Date.now());
|
|
387
|
+
logDebug("autoRecallHook profile ready (V2 injection)", { preferenceCount: injection.preference_count, estimatedTokens: injection.estimated_tokens });
|
|
388
|
+
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
logErr("autoRecallHook getInjection failed, skipping profile", { error: String(e) });
|
|
391
|
+
// profile failure does not block shouldRecall
|
|
393
392
|
}
|
|
394
393
|
}
|
|
395
394
|
|
|
@@ -430,7 +429,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
430
429
|
phase2_multiplier: phase2Multiplier,
|
|
431
430
|
llm_max_eval: llmMaxEval,
|
|
432
431
|
refine_strategy: refineStrategy,
|
|
433
|
-
refine_medium_chars: refineMediumChars,
|
|
434
432
|
},
|
|
435
433
|
directory || process.env.OMEM_PROJECT_DIR,
|
|
436
434
|
);
|
|
@@ -496,8 +494,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
496
494
|
appendToSystem(output.system, profileBlock);
|
|
497
495
|
logDebug("autoRecallHook profile injected (no-recall path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
498
496
|
}
|
|
499
|
-
if (profileInjected &&
|
|
500
|
-
await createEventAndReturn(0, 0, 0);
|
|
497
|
+
if (profileInjected && !lastInjected) {
|
|
501
498
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
|
|
502
499
|
}
|
|
503
500
|
return;
|
|
@@ -514,7 +511,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
514
511
|
appendToSystem(output.system, profileBlock);
|
|
515
512
|
logDebug("autoRecallHook profile injected (dedup path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
|
|
516
513
|
}
|
|
517
|
-
if (profileInjected &&
|
|
514
|
+
if (profileInjected && !lastInjected) {
|
|
518
515
|
showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
|
|
519
516
|
}
|
|
520
517
|
return;
|
|
@@ -526,20 +523,14 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
526
523
|
if (budgetRemaining < 0) {
|
|
527
524
|
logDebug("autoRecallHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
|
|
528
525
|
}
|
|
529
|
-
const itemCount = clustered
|
|
530
|
-
? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
|
|
531
|
-
: newResults.length;
|
|
532
|
-
const dynamicMaxContentLength = itemCount > 0
|
|
533
|
-
? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
|
|
534
|
-
: maxContentLength;
|
|
535
526
|
logDebug("autoRecallHook budget", {
|
|
536
|
-
maxContentChars, profileChars, budgetRemaining,
|
|
537
|
-
configuredMax: maxContentLength,
|
|
527
|
+
maxContentChars, profileChars, budgetRemaining,
|
|
528
|
+
configuredMax: maxContentLength,
|
|
538
529
|
});
|
|
539
530
|
|
|
540
531
|
const block = clustered
|
|
541
|
-
? buildClusteredContextBlock(clustered,
|
|
542
|
-
: buildContextBlock(newResults,
|
|
532
|
+
? buildClusteredContextBlock(clustered, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS)
|
|
533
|
+
: buildContextBlock(newResults, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS);
|
|
543
534
|
if (block) {
|
|
544
535
|
appendToSystem(output.system, block);
|
|
545
536
|
appendToSystem(output.system, FETCH_POLICY);
|
|
@@ -623,537 +614,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
|
|
|
623
614
|
};
|
|
624
615
|
}
|
|
625
616
|
|
|
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
617
|
export function keywordDetectionHook(_client: CerebroClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart", config: Partial<OmemPluginConfig> = {}, agentId?: string) {
|
|
1158
618
|
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
1159
619
|
return async (
|
|
@@ -1297,7 +757,6 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1297
757
|
if (input.sessionID) {
|
|
1298
758
|
sessionMessages.delete(input.sessionID);
|
|
1299
759
|
profileInjectedSessions.delete(input.sessionID);
|
|
1300
|
-
recallCache.delete(input.sessionID);
|
|
1301
760
|
firstMessages.delete(input.sessionID);
|
|
1302
761
|
}
|
|
1303
762
|
return;
|
|
@@ -1329,7 +788,6 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1329
788
|
if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
|
|
1330
789
|
sessionMessages.delete(input.sessionID);
|
|
1331
790
|
profileInjectedSessions.delete(input.sessionID);
|
|
1332
|
-
recallCache.delete(input.sessionID);
|
|
1333
791
|
firstMessages.delete(input.sessionID);
|
|
1334
792
|
} else {
|
|
1335
793
|
const messages = sessionMessages.get(input.sessionID)!;
|
|
@@ -1358,13 +816,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1358
816
|
}
|
|
1359
817
|
// Cleanup tracked messages regardless of ingest result
|
|
1360
818
|
sessionMessages.delete(input.sessionID);
|
|
1361
|
-
injectedSessions.delete(input.sessionID);
|
|
1362
819
|
profileInjectedSessions.delete(input.sessionID);
|
|
1363
|
-
recallCache.delete(input.sessionID);
|
|
1364
820
|
firstMessages.delete(input.sessionID);
|
|
1365
821
|
if (input.sessionID) {
|
|
1366
|
-
|
|
1367
|
-
logDebug("compactingHook cleared session pendingToolCalls", { sessionID: input.sessionID, hadPending: deleted });
|
|
822
|
+
logDebug("compactingHook cleared session state", { sessionID: input.sessionID });
|
|
1368
823
|
}
|
|
1369
824
|
// Evict stale injectedMemoryIds if over size cap (200 sessions)
|
|
1370
825
|
if (injectedMemoryIds.size > 200) {
|
|
@@ -1372,156 +827,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
1372
827
|
}
|
|
1373
828
|
}
|
|
1374
829
|
|
|
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
|
-
}
|
|
830
|
+
// After compacting, clear profile TTL so next autoRecallHook re-injects profile
|
|
831
|
+
if (input.sessionID) {
|
|
832
|
+
profileInjectedSessions.delete(input.sessionID);
|
|
833
|
+
logDebug("compactingHook cleared profile TTL for re-injection", { sessionID: input.sessionID });
|
|
1525
834
|
}
|
|
1526
835
|
};
|
|
1527
836
|
}
|
|
@@ -1639,77 +948,137 @@ export function autocontinueHook(
|
|
|
1639
948
|
const processedMessageIds = new Set<string>();
|
|
1640
949
|
const pluginStartTime = Date.now();
|
|
1641
950
|
|
|
1642
|
-
|
|
1643
|
-
|
|
951
|
+
export function sessionIdleHook(
|
|
952
|
+
cerebroClient: CerebroClient,
|
|
953
|
+
containerTags: string[],
|
|
954
|
+
tui: any,
|
|
955
|
+
sdkClient: any,
|
|
956
|
+
ingestMode: "smart" | "raw" = "smart",
|
|
957
|
+
threshold: number = 0,
|
|
958
|
+
getMainSessionId?: () => string | undefined,
|
|
959
|
+
isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
|
|
960
|
+
agentId?: string,
|
|
961
|
+
config: Partial<OmemPluginConfig> = {},
|
|
962
|
+
onAgentResolved?: (name: string) => void,
|
|
963
|
+
directory?: string,
|
|
964
|
+
) {
|
|
965
|
+
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
966
|
+
let isCapturing = false;
|
|
967
|
+
|
|
968
|
+
async function handleSummaryCapture(props: any) {
|
|
969
|
+
const info = props?.info;
|
|
970
|
+
if (!info) return;
|
|
971
|
+
if (info.role !== "assistant" || !info.summary || !info.finish) return;
|
|
972
|
+
|
|
973
|
+
const sessionID = info.sessionID;
|
|
974
|
+
if (!sessionID) return;
|
|
1644
975
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
976
|
+
if (summarizedSessions.has(sessionID)) return;
|
|
977
|
+
summarizedSessions.add(sessionID);
|
|
978
|
+
|
|
979
|
+
if (!sdkClient) {
|
|
980
|
+
logInfo("handleSummaryCapture skipped: no sdkClient", { sessionID });
|
|
1649
981
|
return;
|
|
1650
982
|
}
|
|
1651
983
|
|
|
1652
|
-
|
|
1653
|
-
const toolName = input.tool;
|
|
984
|
+
logInfo("handleSummaryCapture triggered", { sessionID });
|
|
1654
985
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
986
|
+
if (getMainSessionId) {
|
|
987
|
+
const mainId = getMainSessionId();
|
|
988
|
+
if (mainId && sessionID !== mainId) {
|
|
989
|
+
logInfo("handleSummaryCapture: non-main session skipped", { sessionID, mainSessionId: mainId });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
1659
992
|
}
|
|
1660
993
|
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
994
|
+
const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
|
|
995
|
+
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
996
|
+
if (policy !== "readwrite") {
|
|
997
|
+
logInfo("handleSummaryCapture blocked by policy", { agentId: effectiveAgentId, policy });
|
|
1665
998
|
return;
|
|
1666
999
|
}
|
|
1667
1000
|
|
|
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
|
-
}
|
|
1001
|
+
if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID)) return;
|
|
1678
1002
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1003
|
+
try {
|
|
1004
|
+
const resp = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
1005
|
+
const messages = resp?.data ?? resp;
|
|
1681
1006
|
|
|
1682
|
-
|
|
1007
|
+
const summaryMsg = (messages as Array<{ info: any; parts?: Array<{ type: string; text?: string }> }>).find((m) =>
|
|
1008
|
+
m.info?.role === "assistant" && m.info?.summary === true
|
|
1009
|
+
);
|
|
1683
1010
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1011
|
+
if (!summaryMsg?.parts) {
|
|
1012
|
+
logInfo("handleSummaryCapture: no summary parts found", { sessionID });
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1689
1015
|
|
|
1690
|
-
|
|
1016
|
+
const textParts = summaryMsg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
1017
|
+
const summaryContent = textParts.join("\n").trim();
|
|
1691
1018
|
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1019
|
+
if (!summaryContent || summaryContent.length < 100) {
|
|
1020
|
+
logInfo("handleSummaryCapture: summary too short", { sessionID, length: summaryContent?.length ?? 0 });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1694
1023
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
) {
|
|
1709
|
-
|
|
1710
|
-
|
|
1024
|
+
const effectiveSessionId = getMainSessionId?.() || sessionID;
|
|
1025
|
+
|
|
1026
|
+
let projectName: string | undefined;
|
|
1027
|
+
let projectPath: string | undefined;
|
|
1028
|
+
try {
|
|
1029
|
+
const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
|
|
1030
|
+
projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
|
|
1031
|
+
projectName = sessionInfo?.data?.directory
|
|
1032
|
+
? await detectProjectName(sessionInfo.data.directory)
|
|
1033
|
+
: undefined;
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
logErr("handleSummaryCapture detectProjectName failed", { error: String(e) });
|
|
1036
|
+
}
|
|
1037
|
+
if (!projectPath) {
|
|
1038
|
+
projectPath = directory || process.env.OMEM_PROJECT_DIR;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const prefixedSummary = `[Session Summary] ${summaryContent}`;
|
|
1042
|
+
const result = await cerebroClient.ingestMessages(
|
|
1043
|
+
[{ role: "user" as const, content: prefixedSummary }],
|
|
1044
|
+
{
|
|
1045
|
+
mode: ingestMode,
|
|
1046
|
+
tags: [...containerTags, "auto-capture", "compact-summary"],
|
|
1047
|
+
sessionId: effectiveSessionId,
|
|
1048
|
+
projectName,
|
|
1049
|
+
agentId: effectiveAgentId,
|
|
1050
|
+
projectPath,
|
|
1051
|
+
},
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
logInfo("handleSummaryCapture store result", { result: result === null ? "null(blocked)" : "ok" });
|
|
1055
|
+
if (result !== null) {
|
|
1056
|
+
showToast(tui, "📦 Compact Summary Stored", "Session summary archived", "success");
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
logErr("handleSummaryCapture failed", { error: String(err) });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1711
1062
|
|
|
1712
1063
|
return async (input: { event: { type: string; properties?: any } }) => {
|
|
1064
|
+
if (input.event.type === "message.updated") {
|
|
1065
|
+
await handleSummaryCapture(input.event.properties);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (input.event.type === "session.deleted") {
|
|
1070
|
+
const sessionInfo = input.event.properties?.info;
|
|
1071
|
+
const sid = sessionInfo?.id;
|
|
1072
|
+
if (sid) {
|
|
1073
|
+
summarizedSessions.delete(sid);
|
|
1074
|
+
sessionMessages.delete(sid);
|
|
1075
|
+
profileInjectedSessions.delete(sid);
|
|
1076
|
+
firstMessages.delete(sid);
|
|
1077
|
+
logDebug("sessionIdleHook: session.deleted cleanup", { sessionID: sid });
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1713
1082
|
if (input.event.type !== "session.idle") return;
|
|
1714
1083
|
|
|
1715
1084
|
logDebug("sessionIdleHook event.properties dump", { keys: Object.keys(input.event.properties || {}), raw: JSON.stringify(input.event.properties).substring(0, 2000) });
|
|
@@ -1817,8 +1186,6 @@ export function sessionIdleHook(
|
|
|
1817
1186
|
} finally {
|
|
1818
1187
|
isCapturing = false;
|
|
1819
1188
|
idleTimeout = null;
|
|
1820
|
-
const deleted = pendingToolCalls.delete(sessionID);
|
|
1821
|
-
if (deleted) logDebug("sessionIdleHook cleared session pendingToolCalls", { sessionID, hadPending: deleted });
|
|
1822
1189
|
}
|
|
1823
1190
|
}, 10000);
|
|
1824
1191
|
};
|