@mingxy/cerebro 1.16.9 โ†’ 1.17.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.
Files changed (59) hide show
  1. package/package.json +6 -1
  2. package/src/config.ts +19 -4
  3. package/src/hooks.ts +25 -13
  4. package/src/index.ts +30 -0
  5. package/src/web-server.ts +180 -0
  6. package/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  7. package/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  8. package/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  9. package/web/assets/index-B-0ucKQF.js +119 -0
  10. package/web/assets/index-Dxd5Um3O.css +1 -0
  11. package/web/favicon.svg +1 -0
  12. package/web/icons.svg +24 -0
  13. package/web/index.html +15 -0
  14. package/.omo/evidence/f1-verification.txt +0 -44
  15. package/INJECTION_FLOW.md +0 -434
  16. package/cerebro.example.jsonc +0 -72
  17. package/dist/client.d.ts +0 -165
  18. package/dist/client.d.ts.map +0 -1
  19. package/dist/client.js +0 -222
  20. package/dist/client.js.map +0 -1
  21. package/dist/config.d.ts +0 -46
  22. package/dist/config.d.ts.map +0 -1
  23. package/dist/config.js +0 -201
  24. package/dist/config.js.map +0 -1
  25. package/dist/hooks.d.ts +0 -41
  26. package/dist/hooks.d.ts.map +0 -1
  27. package/dist/hooks.js +0 -1066
  28. package/dist/hooks.js.map +0 -1
  29. package/dist/index.d.ts +0 -11
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js +0 -118
  32. package/dist/index.js.map +0 -1
  33. package/dist/keywords.d.ts +0 -3
  34. package/dist/keywords.d.ts.map +0 -1
  35. package/dist/keywords.js +0 -21
  36. package/dist/keywords.js.map +0 -1
  37. package/dist/logger.d.ts +0 -5
  38. package/dist/logger.d.ts.map +0 -1
  39. package/dist/logger.js +0 -62
  40. package/dist/logger.js.map +0 -1
  41. package/dist/privacy.d.ts +0 -3
  42. package/dist/privacy.d.ts.map +0 -1
  43. package/dist/privacy.js +0 -10
  44. package/dist/privacy.js.map +0 -1
  45. package/dist/tags.d.ts +0 -3
  46. package/dist/tags.d.ts.map +0 -1
  47. package/dist/tags.js +0 -13
  48. package/dist/tags.js.map +0 -1
  49. package/dist/tools.d.ts +0 -209
  50. package/dist/tools.d.ts.map +0 -1
  51. package/dist/tools.js +0 -344
  52. package/dist/tools.js.map +0 -1
  53. package/dist/tui.d.ts +0 -7
  54. package/dist/tui.d.ts.map +0 -1
  55. package/dist/tui.js +0 -63
  56. package/dist/tui.js.map +0 -1
  57. package/mingxy-omem-0.1.6.tgz +0 -0
  58. package/schema.json +0 -225
  59. package/tsconfig.json +0 -26
package/dist/hooks.js DELETED
@@ -1,1066 +0,0 @@
1
- import { resolveAgentPolicy } from "./config.js";
2
- import { detectSaveKeyword, KEYWORD_NUDGE } from "./keywords.js";
3
- import { logDebug, logInfo, logError as logErr } from "./logger.js";
4
- import { readFile } from "node:fs/promises";
5
- import { stripPrivateContent } from "./privacy.js";
6
- const BOUNDARY_SEARCH_RATIO = 0.6;
7
- const MIN_ITEM_CONTENT_CHARS = 100;
8
- const MIN_CONTENT_CHARS = 1000;
9
- const MIN_CONTENT_LENGTH = 50;
10
- const projectNameCache = new Map();
11
- function appendToSystem(system, content) {
12
- if (system.length > 0) {
13
- system[system.length - 1] += "\n\n" + content;
14
- }
15
- else {
16
- system.push(content);
17
- }
18
- }
19
- async function detectProjectName(rootPath) {
20
- const cached = projectNameCache.get(rootPath);
21
- if (cached !== undefined) {
22
- logDebug("detectProjectName cache hit", { rootPath, result: cached });
23
- return cached;
24
- }
25
- let result;
26
- try {
27
- const agents = await readFile(`${rootPath}/AGENTS.md`, "utf-8");
28
- const headingMatch = agents.match(/^#\s+(.+)/m);
29
- if (headingMatch) {
30
- result = headingMatch[1].replace(/\s*\(.*?\)/g, "").trim() || undefined;
31
- }
32
- logDebug("detectProjectName step1 AGENTS.md", { rootPath, result });
33
- }
34
- catch { }
35
- if (!result) {
36
- try {
37
- const pkg = await readFile(`${rootPath}/package.json`, "utf-8");
38
- const nameMatch = pkg.match(/"name"\s*:\s*"([^"]+)"/);
39
- if (nameMatch)
40
- result = nameMatch[1].trim() || undefined;
41
- logDebug("detectProjectName step2 package.json", { rootPath, result });
42
- }
43
- catch { }
44
- }
45
- if (!result) {
46
- try {
47
- const cargo = await readFile(`${rootPath}/Cargo.toml`, "utf-8");
48
- const inPackage = cargo.replace(/\r\n/g, "\n").split("\n").reduce((acc, line) => {
49
- if (/^\[package\]/.test(line.trim()))
50
- return { ...acc, inSection: true };
51
- if (/^\[/.test(line.trim()))
52
- return { ...acc, inSection: false };
53
- if (acc.inSection) {
54
- const m = line.match(/name\s*=\s*"([^"]+)"/);
55
- if (m)
56
- return { ...acc, name: m[1] };
57
- }
58
- return acc;
59
- }, { inSection: false, name: undefined });
60
- result = inPackage.name?.trim() || undefined;
61
- logDebug("detectProjectName step3 Cargo.toml", { rootPath, result });
62
- }
63
- catch { }
64
- }
65
- if (!result) {
66
- try {
67
- const gomod = await readFile(`${rootPath}/go.mod`, "utf-8");
68
- const modMatch = gomod.match(/^module\s+(\S+)/m);
69
- if (modMatch) {
70
- const segments = modMatch[1].split("/");
71
- result = segments.pop()?.trim() || undefined;
72
- }
73
- logDebug("detectProjectName step4 go.mod", { rootPath, result });
74
- }
75
- catch { }
76
- }
77
- if (!result) {
78
- try {
79
- const pyproj = await readFile(`${rootPath}/pyproject.toml`, "utf-8");
80
- const inProject = pyproj.replace(/\r\n/g, "\n").split("\n").reduce((acc, line) => {
81
- if (/^\[project\]/.test(line.trim()))
82
- return { ...acc, inSection: true };
83
- if (/^\[/.test(line.trim()))
84
- return { ...acc, inSection: false };
85
- if (acc.inSection) {
86
- const m = line.match(/name\s*=\s*"([^"]+)"/);
87
- if (m)
88
- return { ...acc, name: m[1] };
89
- }
90
- return acc;
91
- }, { inSection: false, name: undefined });
92
- result = inProject.name?.trim() || undefined;
93
- logDebug("detectProjectName step5 pyproject.toml", { rootPath, result });
94
- }
95
- catch { }
96
- }
97
- if (!result) {
98
- try {
99
- const composer = await readFile(`${rootPath}/composer.json`, "utf-8");
100
- const nameMatch = composer.match(/"name"\s*:\s*"([^"]+)"/);
101
- if (nameMatch)
102
- result = nameMatch[1].trim() || undefined;
103
- logDebug("detectProjectName step6 composer.json", { rootPath, result });
104
- }
105
- catch { }
106
- }
107
- if (!result) {
108
- result = rootPath.split("/").pop() || rootPath.split("\\").pop() || undefined;
109
- logDebug("detectProjectName step7 fallback dirname", { rootPath, result });
110
- }
111
- if (result) {
112
- result = result.trim() || undefined;
113
- }
114
- if (result) {
115
- projectNameCache.set(rootPath, result);
116
- }
117
- return result;
118
- }
119
- export function showToast(tui, title, message, variant = "info", delayMs = 7000) {
120
- if (!tui)
121
- return;
122
- setTimeout(() => {
123
- try {
124
- tui.showToast({ body: { title, message, variant, duration: 5000 } });
125
- }
126
- catch (err) {
127
- logErr("showToast failed", { error: String(err) });
128
- }
129
- }, delayMs);
130
- }
131
- const SYSTEM_INJECTION_PATTERNS = [
132
- /^\[search-mode\]/,
133
- /^\[analyze-mode\]/,
134
- /<!--\s*OMO_INTERNAL_INITIATOR\s*-->/,
135
- /^\[SYSTEM DIRECTIVE:/,
136
- /^\[restore checkpointed/,
137
- /^\[session recovered/,
138
- /^<system-reminder>/,
139
- /^<EXTREMELY_IMPORTANT>/,
140
- /^\[CONTEXT\]/,
141
- /^\[GOAL\]/,
142
- /^## ไปปๅŠก[๏ผš:]/,
143
- /^## ๆ”นๅŠจ/,
144
- /^## ไปปๅŠก๏ผš/,
145
- /^Analyze the attached file/,
146
- /^Provide ONLY the extracted/,
147
- /^Called the Read tool/,
148
- /^MANDATORY delegate_task/,
149
- /^[โ–ฃโ–ช]\s*DCP/,
150
- ];
151
- function extractUserRequest(content) {
152
- const match = content.match(/<user-request>([\s\S]*?)<\/user-request>/);
153
- let text = match ? match[1].trim() : content;
154
- for (const pattern of SYSTEM_INJECTION_PATTERNS) {
155
- if (pattern.test(text))
156
- return "";
157
- }
158
- return text;
159
- }
160
- const saveKeywordDetectedSessions = new Set();
161
- const firstMessages = new Map();
162
- const sessionMessages = new Map();
163
- export const profileInjectedSessions = new Map();
164
- const lastProfileBlock = new Map();
165
- const lastUserMsgCount = new Map();
166
- const summarizedSessions = new Set();
167
- function formatRelativeAge(isoDate) {
168
- const diffMs = Date.now() - new Date(isoDate).getTime();
169
- const minutes = Math.floor(diffMs / 60_000);
170
- if (minutes < 60)
171
- return `${minutes}m ago`;
172
- const hours = Math.floor(minutes / 60);
173
- if (hours < 24)
174
- return `${hours}h ago`;
175
- const days = Math.floor(hours / 24);
176
- if (days < 30)
177
- return `${days}d ago`;
178
- const months = Math.floor(days / 30);
179
- return `${months}mo ago`;
180
- }
181
- function truncate(text, maxLength) {
182
- if (text.length <= maxLength)
183
- return text;
184
- // Sentence boundary characters: period, exclamation, question (Latin + CJK)
185
- // Also treat newline as a boundary
186
- const boundaries = /[.!?ใ€‚๏ผ๏ผŸ\n]/;
187
- // Search backwards from maxLength for a boundary
188
- const searchEnd = Math.min(maxLength, text.length);
189
- for (let i = searchEnd - 1; i >= Math.floor(searchEnd * BOUNDARY_SEARCH_RATIO); i--) {
190
- if (boundaries.test(text[i])) {
191
- return text.slice(0, i + 1).trimEnd() + "โ€ฆ";
192
- }
193
- }
194
- let truncated = text.slice(0, maxLength);
195
- const lastCode = truncated.charCodeAt(truncated.length - 1);
196
- if (lastCode >= 0xD800 && lastCode <= 0xDBFF)
197
- truncated = truncated.slice(0, -1);
198
- return truncated + "โ€ฆ";
199
- }
200
- function categorize(results) {
201
- const groups = new Map();
202
- for (const r of results) {
203
- const cat = r.memory.category || "General";
204
- const label = cat === "preferences"
205
- ? "Preferences"
206
- : cat === "knowledge"
207
- ? "Knowledge"
208
- : cat.charAt(0).toUpperCase() + cat.slice(1);
209
- if (!groups.has(label))
210
- groups.set(label, []);
211
- groups.get(label).push(r);
212
- }
213
- return groups;
214
- }
215
- function formatMemoryLine(r, maxContentLength) {
216
- const age = formatRelativeAge(r.memory.created_at);
217
- const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
218
- const idTag = ` [id:${r.memory.id}]`;
219
- const relTag = r.memory.relations && r.memory.relations.length > 0
220
- ? ` [rel:${r.memory.relations.map((rel) => rel.target_id).join(",")}]`
221
- : "";
222
- const refineTag = r.refine_relevance?.trim() ? ` [${r.refine_relevance.trim()}]` : "";
223
- const content = truncate(r.memory.content, maxContentLength);
224
- return ` - (${age}${idTag}${relTag}${refineTag}${tags}) ${content}`;
225
- }
226
- const FETCH_POLICY = [
227
- "<cerebro-fetch-policy>",
228
- "IMPORTANT: Each memory above is a condensed summary. The full version contains critical details that may change your response quality.",
229
- "You MUST use memory_get(\"id\") to retrieve the complete content, or memory_search(\"query\") to find specific memories before making decisions based on any summary.",
230
- "Do NOT rely on condensed summaries alone โ€” depth of recall determines quality of response.",
231
- "</cerebro-fetch-policy>",
232
- ].join("\n");
233
- function buildContextBlock(results, budget, maxContentLength = 500, minItemChars = MIN_ITEM_CONTENT_CHARS) {
234
- const empty = { text: "", injectedMemoryIds: [], injectedCount: 0 };
235
- if (results.length === 0)
236
- return empty;
237
- const totalScore = results.reduce((sum, r) => sum + r.score, 0);
238
- const grouped = categorize(results);
239
- const sections = [];
240
- for (const [label, items] of grouped) {
241
- const lines = items.map((r) => {
242
- const itemMaxLen = totalScore > 0
243
- ? Math.min(maxContentLength, Math.max(minItemChars, Math.floor((r.score / totalScore) * budget)))
244
- : Math.min(maxContentLength, Math.max(minItemChars, Math.floor(budget / results.length)));
245
- return formatMemoryLine(r, itemMaxLen);
246
- });
247
- sections.push(`[${label}]\n${lines.join("\n")}`);
248
- }
249
- return {
250
- text: [
251
- "<cerebro-context>",
252
- "",
253
- ...sections,
254
- "</cerebro-context>",
255
- ].join("\n"),
256
- injectedMemoryIds: results.map((r) => r.memory.id),
257
- injectedCount: results.length,
258
- };
259
- }
260
- function buildClusteredContextBlock(clustered, budget, maxContentLength = 500, minItemChars = MIN_ITEM_CONTENT_CHARS) {
261
- const sections = [];
262
- const injectedIds = [];
263
- if (clustered.cluster_summaries.length > 0) {
264
- const totalClusterScore = clustered.cluster_summaries.reduce((sum, cs) => sum + cs.relevance_score, 0);
265
- sections.push("## ๐Ÿ“‹ ไธป้ข˜็ฐ‡๏ผˆ่šๅˆ่ฎฐๅฟ†๏ผ‰");
266
- for (const cs of clustered.cluster_summaries) {
267
- const scoreIndicator = cs.relevance_score >= 0.8 ? "โ˜…โ˜…โ˜…" : cs.relevance_score >= 0.6 ? "โ˜…โ˜…" : "โ˜…";
268
- const clusterMaxLen = totalClusterScore > 0
269
- ? Math.min(maxContentLength, Math.max(minItemChars, Math.floor((cs.relevance_score / totalClusterScore) * budget)))
270
- : Math.min(maxContentLength, Math.max(minItemChars, Math.floor(budget / clustered.cluster_summaries.length)));
271
- sections.push(`\n### ${cs.title} (ๆ•ดๅˆ่‡ช${cs.member_count}ๆก่ฎฐๅฟ†) ${scoreIndicator}`);
272
- sections.push(`> ${cs.summary}`);
273
- if (cs.key_memories.length > 0) {
274
- sections.push("**ๆ ธๅฟƒ่ฆ็‚น๏ผš**");
275
- for (const mem of cs.key_memories) {
276
- const idTag = mem.id ? ` [id:${mem.id}]` : "";
277
- const relTag = mem.relations && mem.relations.length > 0
278
- ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
279
- : "";
280
- const importanceBar = mem.importance >= 0.7 ? "โ—" : mem.importance >= 0.4 ? "โ—" : "โ—‹";
281
- const content = truncate(mem.content, clusterMaxLen);
282
- sections.push(`- ${importanceBar}${idTag}${relTag} ${content}`);
283
- if (mem.id)
284
- injectedIds.push(mem.id);
285
- }
286
- }
287
- }
288
- }
289
- if (clustered.standalone_memories.length > 0) {
290
- const standaloneBudget = clustered.cluster_summaries.length === 0
291
- ? budget
292
- : Math.floor(budget * 0.3);
293
- const standaloneMaxLen = Math.min(maxContentLength, Math.max(minItemChars, Math.floor(standaloneBudget / clustered.standalone_memories.length)));
294
- sections.push("\n## ๐Ÿ“Œ ่กฅๅ……ไฟกๆฏ");
295
- for (const mem of clustered.standalone_memories) {
296
- const idTag = mem.id ? ` [id:${mem.id}]` : "";
297
- const relTag = mem.relations && mem.relations.length > 0
298
- ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
299
- : "";
300
- const content = truncate(mem.content, standaloneMaxLen);
301
- sections.push(`-${idTag}${relTag} ${content}`);
302
- if (mem.id)
303
- injectedIds.push(mem.id);
304
- }
305
- }
306
- const totalInjected = clustered.cluster_summaries.reduce((s, cs) => s + cs.member_count, 0) + clustered.standalone_memories.length;
307
- return {
308
- text: sections.length > 0
309
- ? [
310
- "<cerebro-context>",
311
- "",
312
- ...sections,
313
- "</cerebro-context>",
314
- ].join("\n")
315
- : "",
316
- injectedMemoryIds: injectedIds,
317
- injectedCount: totalInjected,
318
- };
319
- }
320
- export function autoRecallHook(client, containerTags, tui, config = {}, getAgentName, directory) {
321
- const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
322
- const maxRecallResults = config.recall?.maxRecallResults ?? 10;
323
- const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
324
- const topkCapMultiplier = config.recall?.topkCapMultiplier ?? 2;
325
- const mmrJaccardThreshold = config.recall?.mmrJaccardThreshold ?? 0.85;
326
- const mmrPenaltyFactor = config.recall?.mmrPenaltyFactor ?? 0.5;
327
- const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
328
- const llmMaxEval = config.recall?.llmMaxEval ?? 15;
329
- const refineStrategy = config.recall?.refineStrategy ?? "balanced";
330
- const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
331
- const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
332
- const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
333
- return async (input, output) => {
334
- if (!input.sessionID)
335
- return;
336
- // 5a: agent memory policy check โ€” skip recall entirely for 'none' agents
337
- const agentId = getAgentName?.() || process.env.OMEM_AGENT_ID || "opencode";
338
- const policy = resolveAgentPolicy(agentId, config);
339
- if (policy === "none")
340
- return;
341
- try {
342
- logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy });
343
- const messages = sessionMessages.get(input.sessionID) ?? [];
344
- const userMessages = messages.filter((m) => m.role === "user");
345
- const prevCount = lastUserMsgCount.get(input.sessionID) ?? 0;
346
- if (userMessages.length <= prevCount) {
347
- logDebug("autoRecallHook skipped: no new user message", { sessionId: input.sessionID, prevCount, currentCount: userMessages.length });
348
- return;
349
- }
350
- lastUserMsgCount.set(input.sessionID, userMessages.length);
351
- // --- Profile Fetch (V2 inject API with TTL gate + module-level cache) ---
352
- const profileTtlMs = config.profile?.ttlMs ?? 300000; // default 5 minutes
353
- const lastInjected = profileInjectedSessions.get(input.sessionID);
354
- const profileTtlExpired = !lastInjected || (Date.now() - lastInjected > profileTtlMs);
355
- let profileBlock = "";
356
- let profileCountText = "";
357
- if (profileTtlExpired) {
358
- const maxRetries = 2;
359
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
360
- try {
361
- const injection = await client.getInjection(directory || process.env.OMEM_PROJECT_DIR);
362
- if (injection?.content) {
363
- profileBlock = injection.content;
364
- profileCountText = `${injection.preference_count} preferences`;
365
- profileInjectedSessions.set(input.sessionID, Date.now());
366
- lastProfileBlock.set(input.sessionID, { content: profileBlock, count: injection.preference_count });
367
- logDebug("autoRecallHook profile fetched (V2 injection)", { preferenceCount: injection.preference_count, estimatedTokens: injection.estimated_tokens });
368
- }
369
- break;
370
- }
371
- catch (e) {
372
- if (attempt < maxRetries) {
373
- logDebug("autoRecallHook getInjection retry", { attempt: attempt + 1, error: String(e) });
374
- }
375
- else {
376
- logErr("autoRecallHook getInjection failed after retries", { error: String(e) });
377
- showToast(tui, "โš ๏ธ Profile Inject Failed", "Preference injection skipped ยท will retry next turn", "error", toastDelayMs);
378
- }
379
- }
380
- }
381
- }
382
- // After compacting, sessionMessages is cleared but firstMessages gets repopulated
383
- // by keywordDetectionHook with compact summary โ€” skip recall in this transient state
384
- if (userMessages.length === 0) {
385
- logDebug("autoRecallHook skipped: no user messages in session (post-compacting?)", { sessionId: input.sessionID });
386
- return;
387
- }
388
- const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
389
- const query_text = extractUserRequest(rawQuery);
390
- if (!query_text) {
391
- logDebug("autoRecallHook filtered system injection (profile already injected above)", { rawQueryPrefix: rawQuery.slice(0, 60) });
392
- return;
393
- }
394
- const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
395
- const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
396
- const conversationContext = userMessages.length >= 2
397
- ? userMessages.slice(-4, -1).map((m) => {
398
- const stripped = stripPrivateContent(m.content);
399
- return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
400
- })
401
- : undefined;
402
- const shouldRecallRes = await client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined, conversationContext && conversationContext.length > 0 ? conversationContext : undefined, {
403
- fetch_multiplier: fetchMultiplier,
404
- topk_cap_multiplier: topkCapMultiplier,
405
- mmr_jaccard_threshold: mmrJaccardThreshold,
406
- mmr_penalty_factor: mmrPenaltyFactor,
407
- phase2_multiplier: phase2Multiplier,
408
- llm_max_eval: llmMaxEval,
409
- refine_strategy: refineStrategy,
410
- }, directory || process.env.OMEM_PROJECT_DIR);
411
- if (!shouldRecallRes) {
412
- showToast(tui, "๐Ÿง  Cerebro Service Unavailable", "Unable to reach memory API ยท check connection", "error", toastDelayMs);
413
- return;
414
- }
415
- logDebug("autoRecallHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, discardedCount: shouldRecallRes.discarded?.length ?? 0, clustered: !!shouldRecallRes.clustered });
416
- const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
417
- const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
418
- const maxScore = storedMemoryIds.length > 0
419
- ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
420
- : 0;
421
- const createEventAndReturn = async (opts) => {
422
- try {
423
- const items = clustered
424
- ? [
425
- ...clustered.cluster_summaries.flatMap((cs) => cs.key_memories.map((mem) => ({
426
- memory_id: mem.id ?? "",
427
- score: cs.relevance_score,
428
- refine_relevance: undefined,
429
- refine_reasoning: undefined,
430
- is_kept: opts.injectedMemoryIds.includes(mem.id ?? ""),
431
- }))),
432
- ...clustered.standalone_memories.map((mem) => ({
433
- memory_id: mem.id ?? "",
434
- score: 0,
435
- refine_relevance: undefined,
436
- refine_reasoning: undefined,
437
- is_kept: opts.injectedMemoryIds.includes(mem.id ?? ""),
438
- })),
439
- ]
440
- : [
441
- ...(shouldRecallRes.memories?.map((r) => ({
442
- memory_id: r.memory.id,
443
- score: r.score,
444
- refine_relevance: r.refine_relevance,
445
- refine_reasoning: r.refine_reasoning,
446
- is_kept: opts.injectedMemoryIds.includes(r.memory.id),
447
- })) ?? []),
448
- ...(shouldRecallRes.discarded?.map((d) => ({
449
- memory_id: d.memory_id,
450
- score: d.score,
451
- refine_relevance: d.refine_relevance,
452
- refine_reasoning: d.refine_reasoning,
453
- is_kept: false,
454
- })) ?? []),
455
- ];
456
- const result = await client.createRecallEvent({
457
- session_id: input.sessionID,
458
- recall_type: "auto",
459
- query_text,
460
- max_score: maxScore,
461
- llm_confidence: shouldRecallRes.confidence ?? 0,
462
- profile_injected: opts.actualProfileInjected,
463
- kept_count: opts.keptCount,
464
- discarded_count: opts.discardedCount,
465
- injected_count: opts.actualInjectedCount,
466
- profile_content: opts.actualProfileContent,
467
- injected_content: opts.injectedContent,
468
- items: items.length > 0 ? items : undefined,
469
- });
470
- return result?.event_id;
471
- }
472
- catch (e) {
473
- logErr("autoRecallHook createRecallEvent failed", { error: String(e) });
474
- return undefined;
475
- }
476
- };
477
- if (!shouldRecallRes.should_recall) {
478
- const didInjectProfile = profileTtlExpired && !!profileBlock;
479
- if (didInjectProfile) {
480
- appendToSystem(output.system, profileBlock);
481
- logDebug("autoRecallHook profile injected (no-recall path)", { sessionId: input.sessionID, outputSystemLength: output.system.length });
482
- createEventAndReturn({
483
- keptCount: 0,
484
- discardedCount: 0,
485
- actualProfileInjected: true,
486
- actualProfileContent: profileBlock,
487
- actualInjectedCount: 0,
488
- injectedMemoryIds: [],
489
- });
490
- showToast(tui, "๐Ÿ‘จ Profile Injected", `${profileCountText} ยท no recall needed`, "success", toastDelayMs);
491
- }
492
- return;
493
- }
494
- const results = shouldRecallRes.memories ?? [];
495
- const clustered = shouldRecallRes.clustered;
496
- // --- Token Budget Calculation ---
497
- const profileChars = profileBlock ? profileBlock.length : 0;
498
- const budgetRemaining = maxContentChars - profileChars;
499
- if (budgetRemaining < 0) {
500
- logDebug("autoRecallHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
501
- }
502
- logDebug("autoRecallHook budget", {
503
- maxContentChars, profileChars, budgetRemaining,
504
- configuredMax: maxContentLength,
505
- });
506
- const ctxResult = clustered
507
- ? buildClusteredContextBlock(clustered, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS)
508
- : buildContextBlock(results, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS);
509
- if (ctxResult.text) {
510
- appendToSystem(output.system, ctxResult.text);
511
- appendToSystem(output.system, FETCH_POLICY);
512
- logDebug("autoRecallHook block injected to output.system", {
513
- sessionId: input.sessionID,
514
- blockPreview: ctxResult.text.slice(0, 200),
515
- outputSystemLength: output.system.length,
516
- });
517
- }
518
- else {
519
- logDebug("autoRecallHook block was EMPTY โ€” no injection", { sessionId: input.sessionID });
520
- }
521
- if (profileBlock) {
522
- appendToSystem(output.system, profileBlock);
523
- logDebug("autoRecallHook profile injected after context", { sessionId: input.sessionID, outputSystemLength: output.system.length });
524
- }
525
- logDebug("autoRecallHook injection complete", { clustered: !!clustered, sessionId: input.sessionID });
526
- const didInjectProfile = !!profileBlock;
527
- const didInjectContext = !!ctxResult.text;
528
- createEventAndReturn({
529
- keptCount: ctxResult.injectedCount,
530
- discardedCount: storedDiscardedIds.length,
531
- injectedContent: didInjectContext ? ctxResult.text : undefined,
532
- actualProfileInjected: didInjectProfile,
533
- actualProfileContent: profileBlock || undefined,
534
- actualInjectedCount: ctxResult.injectedCount,
535
- injectedMemoryIds: ctxResult.injectedMemoryIds,
536
- });
537
- // --- Toast (every branch shows toast) ---
538
- if (didInjectProfile && didInjectContext) {
539
- showToast(tui, "๐Ÿง  Context + Profile Injected", `${profileCountText} ยท recall active`, "success", toastDelayMs);
540
- }
541
- else if (didInjectProfile) {
542
- showToast(tui, "๐Ÿ‘จ Profile Injected", `${profileCountText} ยท no recall needed`, "success", toastDelayMs);
543
- }
544
- else if (didInjectContext) {
545
- showToast(tui, "๐Ÿง  Context Injected", `Recall active ยท profile cached`, "success", toastDelayMs);
546
- }
547
- else {
548
- showToast(tui, "๐Ÿง  Cerebro", "profile cached ยท no recall needed", "info", toastDelayMs);
549
- }
550
- if (saveKeywordDetectedSessions.has(input.sessionID)) {
551
- appendToSystem(output.system, KEYWORD_NUDGE);
552
- saveKeywordDetectedSessions.delete(input.sessionID);
553
- }
554
- }
555
- catch (err) {
556
- const errMsg = err instanceof Error ? err.message : String(err);
557
- if (errMsg.includes("[cerebro]")) {
558
- // Server returned error (500, etc.) with details
559
- const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
560
- if (cleanMsg.startsWith("500")) {
561
- showToast(tui, "๐Ÿง  Cerebro Server Error", cleanMsg.substring(0, 200), "error");
562
- }
563
- else if (cleanMsg.includes("timed out")) {
564
- showToast(tui, "๐Ÿง  Cerebro Service Timeout", cleanMsg.substring(0, 100), "error");
565
- }
566
- else {
567
- showToast(tui, "๐Ÿง  Cerebro Error", cleanMsg.substring(0, 150), "error");
568
- }
569
- }
570
- else if (errMsg.includes("fetch") || errMsg.includes("network")) {
571
- showToast(tui, "๐Ÿง  Cerebro Service Unavailable", "Network error ยท check API connection", "error");
572
- }
573
- else {
574
- showToast(tui, "๐Ÿง  Memory Recall Error", errMsg.substring(0, 100), "error");
575
- }
576
- }
577
- };
578
- }
579
- export function keywordDetectionHook(_client, _containerTags, threshold, _tui, _ingestMode = "smart", config = {}, agentId) {
580
- const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
581
- return async (input, output) => {
582
- const textContent = output.parts
583
- .filter((p) => p.type === "text")
584
- .map((p) => p.text || p.content || "")
585
- .join(" ")
586
- || output.message.content
587
- || "";
588
- if (!firstMessages.has(input.sessionID)) {
589
- firstMessages.set(input.sessionID, textContent);
590
- }
591
- if (detectSaveKeyword(textContent)) {
592
- saveKeywordDetectedSessions.add(input.sessionID);
593
- logDebug("keywordDetectionHook triggered", { sessionId: input.sessionID });
594
- }
595
- const policy = resolveAgentPolicy(effectiveAgentId, config);
596
- if (policy === "none") {
597
- return;
598
- }
599
- if (!sessionMessages.has(input.sessionID)) {
600
- sessionMessages.set(input.sessionID, []);
601
- }
602
- sessionMessages.get(input.sessionID).push({
603
- role: "user",
604
- content: textContent,
605
- });
606
- const messages = sessionMessages.get(input.sessionID);
607
- if (messages.length >= threshold) {
608
- // Threshold reached โ€” messages will be processed on next session.idle
609
- }
610
- };
611
- }
612
- export function createCerebroCompactionPrompt(context, projectMemories) {
613
- const sections = [
614
- "[Cerebro Compaction Context]",
615
- "",
616
- "## 1. User's Original Request",
617
- "Preserve the user's verbatim original request from the conversation above.",
618
- "",
619
- "## 2. Final Goal",
620
- "What is the ultimate objective the user wants to achieve?",
621
- "",
622
- "## 3. Work Completed",
623
- "List all completed work with file paths and technical decisions made.",
624
- "",
625
- "## 4. Remaining Tasks",
626
- "What is still unfinished or pending?",
627
- "",
628
- "## 5. Prohibited Actions",
629
- "Key constraints and forbidden operations to remember.",
630
- "",
631
- "## 6. Existing Project Knowledge",
632
- ];
633
- if (projectMemories.length > 0) {
634
- const memBlock = projectMemories
635
- .slice(0, 10)
636
- .map((r) => {
637
- const content = r.memory.content ?? "";
638
- const truncated = content.length > 200 ? content.slice(0, 200) + "..." : content;
639
- return ` - [${r.memory.category ?? "general"}] ${truncated}`;
640
- })
641
- .join("\n");
642
- sections.push(memBlock);
643
- }
644
- else {
645
- sections.push(" (No project memories retrieved)");
646
- }
647
- if (context.length > 0) {
648
- sections.push("");
649
- sections.push("### Additional Context");
650
- sections.push(...context);
651
- }
652
- sections.push("");
653
- sections.push("IMPORTANT: Output must preserve the user's original language (Chinese/English/etc). Do not translate.");
654
- return sections.join("\n");
655
- }
656
- export function compactingHook(client, containerTags, tui, ingestMode = "smart", isAutoStoreEnabled, getMainSessionId, sdkClient, config = {}, agentId, directory) {
657
- const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
658
- return async (input, output) => {
659
- logInfo("compactingHook triggered", { sessionId: input.sessionID, hasSessionMessages: sessionMessages.has(input.sessionID || "") });
660
- // Search (read) always runs โ€” even readonly agents need context during compacting
661
- try {
662
- const results = await client.searchMemories("*", 20, undefined, containerTags);
663
- const compactionPrompt = createCerebroCompactionPrompt(output.context, results);
664
- if (output.prompt !== undefined) {
665
- output.prompt = compactionPrompt;
666
- }
667
- else if (output.context.length > 0) {
668
- output.context[output.context.length - 1] += "\n\n" + compactionPrompt;
669
- }
670
- else {
671
- output.context.push(compactionPrompt);
672
- }
673
- if (output.context.length > 0) {
674
- output.context[output.context.length - 1] += "\n\n" + FETCH_POLICY;
675
- }
676
- else {
677
- output.context.push(FETCH_POLICY);
678
- }
679
- }
680
- catch {
681
- }
682
- // Main session gate: sub-agents must not write memories via compacting
683
- if (getMainSessionId) {
684
- const mainId = getMainSessionId();
685
- if (mainId && input.sessionID && input.sessionID !== mainId) {
686
- logInfo("compactingHook: non-main session skipped", { sessionID: input.sessionID, mainSessionId: mainId });
687
- return;
688
- }
689
- }
690
- // Policy gate: only readwrite agents can write memories
691
- const policy = resolveAgentPolicy(effectiveAgentId, config);
692
- if (policy !== "readwrite") {
693
- logInfo("compactingHook blocked by policy", { agentId: effectiveAgentId, policy });
694
- if (input.sessionID) {
695
- sessionMessages.delete(input.sessionID);
696
- profileInjectedSessions.delete(input.sessionID);
697
- lastUserMsgCount.delete(input.sessionID);
698
- firstMessages.delete(input.sessionID);
699
- }
700
- return;
701
- }
702
- const effectiveSessionId = (getMainSessionId?.() || input.sessionID);
703
- // Resolve project name (shared by ingest + poll)
704
- let projectName;
705
- let projectPath;
706
- try {
707
- if (sdkClient && input.sessionID) {
708
- const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
709
- logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
710
- projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
711
- projectName = sessionInfo?.data?.directory
712
- ? await detectProjectName(sessionInfo.data.directory)
713
- : undefined;
714
- }
715
- }
716
- catch (e) {
717
- logErr("compactingHook detectProjectName failed", { error: String(e) });
718
- }
719
- if (!projectPath) {
720
- projectPath = directory || process.env.OMEM_PROJECT_DIR;
721
- }
722
- // --- Phase 1: Ingest tracked messages from sessionMessages (if available) ---
723
- if (input.sessionID && sessionMessages.has(input.sessionID)) {
724
- if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
725
- sessionMessages.delete(input.sessionID);
726
- profileInjectedSessions.delete(input.sessionID);
727
- lastUserMsgCount.delete(input.sessionID);
728
- firstMessages.delete(input.sessionID);
729
- }
730
- else {
731
- const messages = sessionMessages.get(input.sessionID);
732
- if (messages.length > 0) {
733
- try {
734
- logInfo("compactingHook ingestMessages called", { msgCount: messages.length, sessionId: effectiveSessionId, agentId: effectiveAgentId });
735
- const result = await client.ingestMessages(messages, {
736
- mode: ingestMode,
737
- tags: [...containerTags, "auto-capture"],
738
- sessionId: effectiveSessionId,
739
- projectName: projectName,
740
- agentId: effectiveAgentId,
741
- projectPath,
742
- });
743
- logInfo("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
744
- if (result === null) {
745
- showToast(tui, "๐Ÿ”ด Archive Failed", "Session archive blocked ยท check spiritual realm status", "error");
746
- }
747
- else {
748
- showToast(tui, "๐Ÿ“ฆ Session Archived", `${messages.length} residual dialogues archived ยท merged into the realm`, "success");
749
- }
750
- }
751
- catch (e) {
752
- logErr("compactingHook ingestMessages failed", { error: String(e) });
753
- showToast(tui, "๐Ÿ”ด Archive Failed", "Session archive blocked ยท spiritual pulse anomaly", "error");
754
- }
755
- }
756
- }
757
- // Cleanup tracked messages regardless of ingest result
758
- sessionMessages.delete(input.sessionID);
759
- profileInjectedSessions.delete(input.sessionID);
760
- lastUserMsgCount.delete(input.sessionID);
761
- firstMessages.delete(input.sessionID);
762
- if (input.sessionID) {
763
- logDebug("compactingHook cleared session state", { sessionID: input.sessionID });
764
- }
765
- }
766
- // After compacting, clear profile TTL so next autoRecallHook re-injects profile
767
- if (input.sessionID) {
768
- profileInjectedSessions.delete(input.sessionID);
769
- lastUserMsgCount.delete(input.sessionID);
770
- logDebug("compactingHook cleared profile TTL for re-injection", { sessionID: input.sessionID });
771
- }
772
- };
773
- }
774
- export function autocontinueHook(client, containerTags, tui, ingestMode = "smart", isAutoStoreEnabled, getMainSessionId, sdkClient, config = {}, agentId, directory) {
775
- const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
776
- return async (input, _output) => {
777
- try {
778
- const policy = resolveAgentPolicy(effectiveAgentId, config);
779
- if (policy !== "readwrite") {
780
- logInfo("autocontinueHook blocked by policy", { agentId: effectiveAgentId, policy });
781
- return;
782
- }
783
- if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
784
- logInfo("autocontinueHook skipped: auto-store disabled", { sessionId: input.sessionID });
785
- return;
786
- }
787
- const effectiveSessionId = getMainSessionId?.() || input.sessionID;
788
- if (!sdkClient) {
789
- logInfo("autocontinueHook skipped: no sdkClient", { sessionId: input.sessionID });
790
- return;
791
- }
792
- let summaryText;
793
- try {
794
- const response = await sdkClient.session.messages({ path: { id: input.sessionID } });
795
- if (response?.data) {
796
- const targetMsg = response.data.find((msg) => msg.info?.id === input.message.id);
797
- if (targetMsg?.parts) {
798
- const textParts = targetMsg.parts
799
- .filter((p) => p.type === "text" && p.text)
800
- .map((p) => p.text);
801
- summaryText = textParts.join("\n").trim();
802
- }
803
- }
804
- }
805
- catch (e) {
806
- logErr("autocontinueHook failed to fetch message parts", { error: String(e) });
807
- }
808
- if (!summaryText) {
809
- logInfo("autocontinueHook skipped: no summary text found", { sessionId: input.sessionID, messageId: input.message.id });
810
- return;
811
- }
812
- let projectName;
813
- let projectPath;
814
- try {
815
- const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
816
- projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
817
- projectName = sessionInfo?.data?.directory
818
- ? await detectProjectName(sessionInfo.data.directory)
819
- : undefined;
820
- }
821
- catch (e) {
822
- logErr("autocontinueHook detectProjectName failed", { error: String(e) });
823
- }
824
- if (!projectPath) {
825
- projectPath = directory || process.env.OMEM_PROJECT_DIR;
826
- }
827
- const messages = [{ role: "user", content: summaryText }];
828
- logInfo("autocontinueHook storing compact summary", {
829
- summaryLen: summaryText.length,
830
- sessionId: effectiveSessionId,
831
- agentId: effectiveAgentId,
832
- overflow: input.overflow,
833
- projectName,
834
- });
835
- const result = await client.ingestMessages(messages, {
836
- mode: ingestMode,
837
- tags: [...containerTags, "auto-capture", "compact-summary"],
838
- sessionId: effectiveSessionId,
839
- projectName: projectName,
840
- agentId: effectiveAgentId,
841
- projectPath,
842
- });
843
- logInfo("autocontinueHook store result", { result: result === null ? "null(blocked)" : "ok" });
844
- if (result === null) {
845
- showToast(tui, "๐Ÿ”ด Compact Summary Failed", "Storage blocked ยท check server status", "error");
846
- }
847
- else {
848
- showToast(tui, "๐Ÿ“ฆ Compact Summary Stored", "Session summary archived to memory", "success");
849
- }
850
- }
851
- catch (e) {
852
- logErr("autocontinueHook failed", { error: String(e) });
853
- }
854
- };
855
- }
856
- const processedMessageIds = new Set();
857
- const pluginStartTime = Date.now();
858
- export function sessionIdleHook(cerebroClient, containerTags, tui, sdkClient, ingestMode = "smart", threshold = 0, getMainSessionId, isAutoStoreEnabled, agentId, config = {}, onAgentResolved, directory) {
859
- let idleTimeout = null;
860
- let isCapturing = false;
861
- async function handleSummaryCapture(props) {
862
- const info = props?.info;
863
- if (!info)
864
- return;
865
- if (info.role !== "assistant" || !info.summary || !info.finish)
866
- return;
867
- const sessionID = info.sessionID;
868
- if (!sessionID)
869
- return;
870
- if (summarizedSessions.has(sessionID))
871
- return;
872
- summarizedSessions.add(sessionID);
873
- if (!sdkClient) {
874
- logInfo("handleSummaryCapture skipped: no sdkClient", { sessionID });
875
- return;
876
- }
877
- logInfo("handleSummaryCapture triggered", { sessionID });
878
- if (getMainSessionId) {
879
- const mainId = getMainSessionId();
880
- if (mainId && sessionID !== mainId) {
881
- logInfo("handleSummaryCapture: non-main session skipped", { sessionID, mainSessionId: mainId });
882
- return;
883
- }
884
- }
885
- const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
886
- const policy = resolveAgentPolicy(effectiveAgentId, config);
887
- if (policy !== "readwrite") {
888
- logInfo("handleSummaryCapture blocked by policy", { agentId: effectiveAgentId, policy });
889
- return;
890
- }
891
- if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID))
892
- return;
893
- try {
894
- const resp = await sdkClient.session.messages({ path: { id: sessionID } });
895
- const messages = resp?.data ?? resp;
896
- const summaryMsg = messages.find((m) => m.info?.role === "assistant" && m.info?.summary === true);
897
- if (!summaryMsg?.parts) {
898
- logInfo("handleSummaryCapture: no summary parts found", { sessionID });
899
- return;
900
- }
901
- const textParts = summaryMsg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
902
- const summaryContent = textParts.join("\n").trim();
903
- if (!summaryContent || summaryContent.length < 100) {
904
- logInfo("handleSummaryCapture: summary too short", { sessionID, length: summaryContent?.length ?? 0 });
905
- return;
906
- }
907
- const effectiveSessionId = getMainSessionId?.() || sessionID;
908
- let projectName;
909
- let projectPath;
910
- try {
911
- const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
912
- projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
913
- projectName = sessionInfo?.data?.directory
914
- ? await detectProjectName(sessionInfo.data.directory)
915
- : undefined;
916
- }
917
- catch (e) {
918
- logErr("handleSummaryCapture detectProjectName failed", { error: String(e) });
919
- }
920
- if (!projectPath) {
921
- projectPath = directory || process.env.OMEM_PROJECT_DIR;
922
- }
923
- const prefixedSummary = `[Session Summary] ${summaryContent}`;
924
- const result = await cerebroClient.ingestMessages([{ role: "user", content: prefixedSummary }], {
925
- mode: ingestMode,
926
- tags: [...containerTags, "auto-capture", "compact-summary"],
927
- sessionId: effectiveSessionId,
928
- projectName,
929
- agentId: effectiveAgentId,
930
- projectPath,
931
- });
932
- logInfo("handleSummaryCapture store result", { result: result === null ? "null(blocked)" : "ok" });
933
- if (result !== null) {
934
- showToast(tui, "๐Ÿ“ฆ Compact Summary Stored", "Session summary archived", "success");
935
- }
936
- }
937
- catch (err) {
938
- logErr("handleSummaryCapture failed", { error: String(err) });
939
- }
940
- }
941
- return async (input) => {
942
- if (input.event.type === "message.updated") {
943
- await handleSummaryCapture(input.event.properties);
944
- return;
945
- }
946
- if (input.event.type === "session.deleted") {
947
- const sessionInfo = input.event.properties?.info;
948
- const sid = sessionInfo?.id;
949
- if (sid) {
950
- summarizedSessions.delete(sid);
951
- sessionMessages.delete(sid);
952
- profileInjectedSessions.delete(sid);
953
- lastUserMsgCount.delete(sid);
954
- firstMessages.delete(sid);
955
- logDebug("sessionIdleHook: session.deleted cleanup", { sessionID: sid });
956
- }
957
- return;
958
- }
959
- if (input.event.type !== "session.idle")
960
- return;
961
- logDebug("sessionIdleHook event.properties dump", { keys: Object.keys(input.event.properties || {}), raw: JSON.stringify(input.event.properties).substring(0, 2000) });
962
- const sessionID = input.event.properties?.sessionID;
963
- if (!sessionID)
964
- return;
965
- if (isAutoStoreEnabled && !isAutoStoreEnabled(sessionID))
966
- return;
967
- if (getMainSessionId) {
968
- const mainId = getMainSessionId();
969
- if (mainId && sessionID !== mainId) {
970
- logInfo("sessionIdleHook: non-main session skipped", { sessionID, mainSessionId: mainId });
971
- return;
972
- }
973
- }
974
- if (idleTimeout)
975
- clearTimeout(idleTimeout);
976
- idleTimeout = setTimeout(async () => {
977
- if (isCapturing)
978
- return;
979
- isCapturing = true;
980
- try {
981
- const response = await sdkClient.session.messages({ path: { id: sessionID } });
982
- if (!response?.data)
983
- return;
984
- const messages = response.data;
985
- const conversationMessages = [];
986
- const newMessageIds = [];
987
- let hasNewMessages = false;
988
- for (const msg of messages) {
989
- const msgId = msg.info?.id;
990
- if (!msgId || processedMessageIds.has(msgId))
991
- continue;
992
- const msgTime = msg.info?.createdAt ? new Date(msg.info.createdAt).getTime() : 0;
993
- if (msgTime > 0 && msgTime < pluginStartTime)
994
- continue;
995
- const role = msg.info?.role;
996
- if (role !== "user" && role !== "assistant")
997
- continue;
998
- const textParts = (msg.parts || [])
999
- .filter((p) => p.type === "text" && p.text)
1000
- .map((p) => p.text);
1001
- const text = textParts.join("\n").trim();
1002
- if (!text)
1003
- continue;
1004
- hasNewMessages = true;
1005
- newMessageIds.push(msgId);
1006
- conversationMessages.push({ role, content: text });
1007
- }
1008
- if (!hasNewMessages || conversationMessages.length === 0)
1009
- return;
1010
- if (threshold > 1 && conversationMessages.length < threshold) {
1011
- return;
1012
- }
1013
- let sessionTitle;
1014
- let projectName;
1015
- let projectPath;
1016
- let effectiveAgentId = agentId || "opencode";
1017
- try {
1018
- const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
1019
- if (sessionInfo?.data?.agent) {
1020
- effectiveAgentId = sessionInfo.data.agent;
1021
- onAgentResolved?.(effectiveAgentId);
1022
- }
1023
- sessionTitle = sessionInfo?.data?.title;
1024
- projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
1025
- projectName = sessionInfo?.data?.directory
1026
- ? await detectProjectName(sessionInfo.data.directory)
1027
- : undefined;
1028
- }
1029
- catch (e) {
1030
- logErr("sessionIdleHook detectProjectName failed", { error: String(e) });
1031
- }
1032
- if (!projectPath) {
1033
- projectPath = directory || process.env.OMEM_PROJECT_DIR;
1034
- }
1035
- logDebug("sessionIdleHook resolved agentId", { effectiveAgentId, fallbackAgentId: agentId });
1036
- const policy = resolveAgentPolicy(effectiveAgentId, config);
1037
- if (policy !== "readwrite") {
1038
- logInfo("sessionIdleHook blocked by policy", { agentId: effectiveAgentId, policy, defaultPolicy: String(config.defaultPolicy ?? "undefined") });
1039
- return;
1040
- }
1041
- try {
1042
- logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, agentId: effectiveAgentId, title: String(sessionTitle) });
1043
- await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName, projectPath);
1044
- logInfo("sessionIdleHook sessionIngest ok");
1045
- for (const id of newMessageIds) {
1046
- processedMessageIds.add(id);
1047
- }
1048
- showToast(tui, "๐Ÿง  Memory Sealed", `${conversationMessages.length} dialogues captured ยท entrusted to the heavens for refinement`, "success");
1049
- }
1050
- catch (err) {
1051
- logErr("sessionIdleHook sessionIngest failed", { error: String(err) });
1052
- showToast(tui, "๐Ÿ”ด Session Capture Failed", String(err).substring(0, 100), "error");
1053
- }
1054
- }
1055
- catch (err) {
1056
- const errMsg = err instanceof Error ? err.message : String(err);
1057
- showToast(tui, "๐Ÿ”ด Idle Capture Error", errMsg.substring(0, 100), "error");
1058
- }
1059
- finally {
1060
- isCapturing = false;
1061
- idleTimeout = null;
1062
- }
1063
- }, 10000);
1064
- };
1065
- }
1066
- //# sourceMappingURL=hooks.js.map