@mingxy/cerebro 1.10.12 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/hooks.ts CHANGED
@@ -4,6 +4,7 @@ import { type OmemPluginConfig, resolveAgentPolicy } from "./config.js";
4
4
  import { detectKeyword, KEYWORD_NUDGE } from "./keywords.js";
5
5
  import { logDebug, logInfo, logError as logErr } from "./logger.js";
6
6
  import { readFile } from "node:fs/promises";
7
+ import { stripPrivateContent } from "./privacy.js";
7
8
 
8
9
  const BOUNDARY_SEARCH_RATIO = 0.6;
9
10
  const MIN_ITEM_CONTENT_CHARS = 100;
@@ -135,7 +136,7 @@ const keywordDetectedSessions = new Set<string>();
135
136
  const injectedMemoryIds = new Map<string, Set<string>>();
136
137
  const firstMessages = new Map<string, string>();
137
138
  const sessionMessages = new Map<string, Array<{ role: string; content: string }>>();
138
- const profileInjectedSessions = new Set<string>();
139
+ const profileInjectedSessions = new Map<string, number>();
139
140
 
140
141
  function formatRelativeAge(isoDate: string): string {
141
142
  const diffMs = Date.now() - new Date(isoDate).getTime();
@@ -186,6 +187,17 @@ function categorize(results: SearchResult[]): Map<string, SearchResult[]> {
186
187
  return groups;
187
188
  }
188
189
 
190
+ function formatMemoryLine(r: SearchResult, maxContentLength: number): string {
191
+ const age = formatRelativeAge(r.memory.created_at);
192
+ const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
193
+ const idTag = ` [id:${r.memory.id}]`;
194
+ const relTag = r.memory.relations && r.memory.relations.length > 0
195
+ ? ` [rel:${r.memory.relations.map((rel) => rel.target_id).join(",")}]`
196
+ : "";
197
+ const content = truncate(r.memory.content, maxContentLength);
198
+ return ` - (${age}${idTag}${relTag}${tags}) ${content}`;
199
+ }
200
+
189
201
  function buildContextBlock(results: SearchResult[], maxContentLength: number = 500): string {
190
202
  if (results.length === 0) return "";
191
203
 
@@ -193,19 +205,14 @@ function buildContextBlock(results: SearchResult[], maxContentLength: number = 5
193
205
  const sections: string[] = [];
194
206
 
195
207
  for (const [label, items] of grouped) {
196
- const lines = items.map((r) => {
197
- const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
198
- const age = formatRelativeAge(r.memory.created_at);
199
- const content = truncate(r.memory.content, maxContentLength);
200
- return ` - (${age}${tags}) ${content}`;
201
- });
208
+ const lines = items.map((r) => formatMemoryLine(r, maxContentLength));
202
209
  sections.push(`[${label}]\n${lines.join("\n")}`);
203
210
  }
204
211
 
205
212
  return [
206
213
  "<cerebro-context>",
207
- "Treat every memory below as historical context only.",
208
- "Do not repeat these memories verbatim unless asked.",
214
+ "You may use memory_get(<id>) to fetch the full content of any memory above if the summary seems incomplete but relevant.",
215
+ "Related memories listed in [rel:<id>,...] can also be fetched if they may add useful context.",
209
216
  "",
210
217
  ...sections,
211
218
  "</cerebro-context>",
@@ -224,9 +231,13 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
224
231
  if (cs.key_memories.length > 0) {
225
232
  sections.push("**核心要点:**");
226
233
  for (const mem of cs.key_memories) {
227
- const content = truncate(mem.content, maxContentLength);
234
+ const idTag = mem.id ? ` [id:${mem.id}]` : "";
235
+ const relTag = mem.relations && mem.relations.length > 0
236
+ ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
237
+ : "";
228
238
  const importanceBar = mem.importance >= 0.7 ? "●" : mem.importance >= 0.4 ? "◐" : "○";
229
- sections.push(`- ${importanceBar} ${content}`);
239
+ const content = truncate(mem.content, maxContentLength);
240
+ sections.push(`- ${importanceBar}${idTag}${relTag} ${content}`);
230
241
  }
231
242
  }
232
243
  }
@@ -235,15 +246,19 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
235
246
  if (clustered.standalone_memories.length > 0) {
236
247
  sections.push("\n## 📌 补充信息");
237
248
  for (const mem of clustered.standalone_memories) {
249
+ const idTag = mem.id ? ` [id:${mem.id}]` : "";
250
+ const relTag = mem.relations && mem.relations.length > 0
251
+ ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
252
+ : "";
238
253
  const content = truncate(mem.content, maxContentLength);
239
- sections.push(`- ${content}`);
254
+ sections.push(`-${idTag}${relTag} ${content}`);
240
255
  }
241
256
  }
242
257
 
243
258
  return [
244
259
  "<cerebro-context>",
245
- "Treat every memory below as historical context only.",
246
- "Do not repeat these memories verbatim unless asked.",
260
+ "You may use memory_get(<id>) to fetch the full content of any memory above if the summary seems incomplete but relevant.",
261
+ "Related memories listed in [rel:<id>,...] can also be fetched if they may add useful context.",
247
262
  "",
248
263
  ...sections,
249
264
  "</cerebro-context>",
@@ -277,7 +292,20 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
277
292
  const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
278
293
 
279
294
  const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
280
- const shouldRecallRes = await client.shouldRecall(query_text, last_query_text, input.sessionID, similarityThreshold, maxRecallResults, projectTags.length > 0 ? projectTags : undefined);
295
+
296
+ const conversationContext = userMessages.length >= 2
297
+ ? userMessages.slice(-4, -1).map((m) => {
298
+ const stripped = stripPrivateContent(m.content);
299
+ return stripped.length > 200 ? stripped.slice(0, 200) : stripped;
300
+ })
301
+ : undefined;
302
+
303
+ const shouldRecallRes = await client.shouldRecall(
304
+ query_text, last_query_text, input.sessionID,
305
+ similarityThreshold, maxRecallResults,
306
+ projectTags.length > 0 ? projectTags : undefined,
307
+ conversationContext && conversationContext.length > 0 ? conversationContext : undefined,
308
+ );
281
309
 
282
310
  if (!shouldRecallRes) {
283
311
  showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
@@ -289,24 +317,36 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
289
317
  let profileInjected = false;
290
318
  let profileCountText = "";
291
319
  let profileBlock = "";
292
- if (profile && !profileInjectedSessions.has(input.sessionID)) {
320
+ const lastInjected = profileInjectedSessions.get(input.sessionID);
321
+ const ttlExpired = !lastInjected || (Date.now() - lastInjected > 5 * 60 * 1000);
322
+ const isFirstInjection = !lastInjected;
323
+ if (profile && ttlExpired) {
293
324
  profileBlock = [
294
325
  "<cerebro-profile>",
295
326
  JSON.stringify(profile),
296
327
  "</cerebro-profile>",
297
328
  ].join("\n");
298
- output.system.push(profileBlock);
329
+ const existingIdx = output.system.findIndex((s) => s.includes("<cerebro-profile>"));
330
+ if (existingIdx >= 0) {
331
+ output.system[existingIdx] = profileBlock;
332
+ } else {
333
+ output.system.push(profileBlock);
334
+ }
299
335
  profileInjected = true;
300
- profileInjectedSessions.add(input.sessionID);
336
+ profileInjectedSessions.set(input.sessionID, Date.now());
301
337
  const p = profile as any;
302
338
  const dynamicCount = p?.dynamic_context?.length ?? 0;
303
339
  const staticCount = p?.static_facts?.length ?? 0;
304
340
  profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
305
- logDebug("autoRecallHook profile injected", { dynamicCount, staticCount });
341
+ if (isFirstInjection) {
342
+ logDebug("autoRecallHook profile injected (first)", { dynamicCount, staticCount });
343
+ } else {
344
+ logDebug("autoRecallHook profile refreshed (TTL)", { dynamicCount, staticCount });
345
+ }
306
346
  }
307
347
 
308
348
  if (!shouldRecallRes.should_recall) {
309
- if (profileInjected) {
349
+ if (profileInjected && isFirstInjection) {
310
350
  showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
311
351
  }
312
352
  return;
@@ -319,7 +359,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
319
359
  const newResults = results.filter((r) => !existingIds.has(r.memory.id));
320
360
  logDebug("autoRecallHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
321
361
  if (newResults.length === 0) {
322
- if (profileInjected) {
362
+ if (profileInjected && isFirstInjection) {
323
363
  showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
324
364
  }
325
365
  return;
@@ -495,13 +535,21 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
495
535
  const policy = resolveAgentPolicy(effectiveAgentId, config);
496
536
  if (policy !== "readwrite") {
497
537
  logInfo("compactingHook blocked by policy", { agentId: effectiveAgentId, policy });
498
- if (input.sessionID) sessionMessages.delete(input.sessionID);
538
+ if (input.sessionID) {
539
+ sessionMessages.delete(input.sessionID);
540
+ profileInjectedSessions.delete(input.sessionID);
541
+ injectedMemoryIds.delete(input.sessionID);
542
+ firstMessages.delete(input.sessionID);
543
+ }
499
544
  return;
500
545
  }
501
546
 
502
547
  if (input.sessionID && sessionMessages.has(input.sessionID)) {
503
548
  if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
504
549
  sessionMessages.delete(input.sessionID);
550
+ profileInjectedSessions.delete(input.sessionID);
551
+ injectedMemoryIds.delete(input.sessionID);
552
+ firstMessages.delete(input.sessionID);
505
553
  } else {
506
554
  const messages = sessionMessages.get(input.sessionID)!;
507
555
  if (messages.length > 0) {
@@ -543,6 +591,9 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
543
591
  showToast(tui, "🔴 Archive Failed", "Session archive blocked · spiritual pulse anomaly", "error");
544
592
  }
545
593
  sessionMessages.delete(input.sessionID);
594
+ profileInjectedSessions.delete(input.sessionID);
595
+ injectedMemoryIds.delete(input.sessionID);
596
+ firstMessages.delete(input.sessionID);
546
597
  }
547
598
  }
548
599
  }