@mingxy/cerebro 1.15.14 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/cerebro",
3
- "version": "1.15.14",
3
+ "version": "1.16.0",
4
4
  "description": "Cerebro persistent memory plugin for OpenCode — auto-recall, auto-capture, 9 memory tools with clustering, project-scoped memory isolation",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/schema.json CHANGED
@@ -144,13 +144,6 @@
144
144
  "description": "LLM refinement strategy: strict (high only), balanced (high+medium), loose (keep all)",
145
145
  "enum": ["strict", "balanced", "loose"],
146
146
  "default": "balanced"
147
- },
148
- "refineMediumChars": {
149
- "type": "number",
150
- "description": "Character limit for medium-relevance content truncation",
151
- "default": 200,
152
- "minimum": 50,
153
- "maximum": 2000
154
147
  }
155
148
  },
156
149
  "additionalProperties": false
package/src/client.ts CHANGED
@@ -363,7 +363,6 @@ export class CerebroClient {
363
363
  phase2_multiplier?: number;
364
364
  llm_max_eval?: number;
365
365
  refine_strategy?: string;
366
- refine_medium_chars?: number;
367
366
  skip_llm_gate?: boolean;
368
367
  },
369
368
  projectPath?: string,
package/src/config.ts CHANGED
@@ -29,7 +29,6 @@ export interface OmemPluginConfig {
29
29
  phase2Multiplier: number;
30
30
  llmMaxEval: number;
31
31
  refineStrategy: "strict" | "balanced" | "loose";
32
- refineMediumChars: number;
33
32
  };
34
33
  logging: {
35
34
  logEnabled: boolean;
@@ -73,7 +72,6 @@ const DEFAULTS: OmemPluginConfig = {
73
72
  phase2Multiplier: 2,
74
73
  llmMaxEval: 15,
75
74
  refineStrategy: "balanced",
76
- refineMediumChars: 200,
77
75
  },
78
76
  logging: {
79
77
  logEnabled: true,
@@ -140,7 +138,6 @@ function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
140
138
  phase2Multiplier: DEFAULTS.recall.phase2Multiplier,
141
139
  llmMaxEval: DEFAULTS.recall.llmMaxEval,
142
140
  refineStrategy: DEFAULTS.recall.refineStrategy,
143
- refineMediumChars: DEFAULTS.recall.refineMediumChars,
144
141
  },
145
142
  logging: {
146
143
  logEnabled: flat.logEnabled ?? DEFAULTS.logging.logEnabled,
package/src/hooks.ts CHANGED
@@ -243,14 +243,30 @@ const FETCH_POLICY = [
243
243
  "</cerebro-fetch-policy>",
244
244
  ].join("\n");
245
245
 
246
- function buildContextBlock(results: SearchResult[], maxContentLength: number = 500): string {
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 {
247
256
  if (results.length === 0) return "";
248
257
 
258
+ const totalScore = results.reduce((sum, r) => sum + r.score, 0);
259
+
249
260
  const grouped = categorize(results);
250
261
  const sections: string[] = [];
251
262
 
252
263
  for (const [label, items] of grouped) {
253
- const lines = items.map((r) => formatMemoryLine(r, maxContentLength));
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
+ });
254
270
  sections.push(`[${label}]\n${lines.join("\n")}`);
255
271
  }
256
272
 
@@ -262,13 +278,23 @@ function buildContextBlock(results: SearchResult[], maxContentLength: number = 5
262
278
  ].join("\n");
263
279
  }
264
280
 
265
- function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRecallResult, maxContentLength: number = 500): string {
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 {
266
287
  const sections: string[] = [];
267
288
 
268
289
  if (clustered.cluster_summaries.length > 0) {
290
+ const totalClusterScore = clustered.cluster_summaries.reduce((sum, cs) => sum + cs.relevance_score, 0);
291
+
269
292
  sections.push("## 📋 主题簇(聚合记忆)");
270
293
  for (const cs of clustered.cluster_summaries) {
271
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)));
272
298
  sections.push(`\n### ${cs.title} (整合自${cs.member_count}条记忆) ${scoreIndicator}`);
273
299
  sections.push(`> ${cs.summary}`);
274
300
  if (cs.key_memories.length > 0) {
@@ -279,7 +305,7 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
279
305
  ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
280
306
  : "";
281
307
  const importanceBar = mem.importance >= 0.7 ? "●" : mem.importance >= 0.4 ? "◐" : "○";
282
- const content = truncate(mem.content, maxContentLength);
308
+ const content = truncate(mem.content, clusterMaxLen);
283
309
  sections.push(`- ${importanceBar}${idTag}${relTag} ${content}`);
284
310
  }
285
311
  }
@@ -287,13 +313,18 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
287
313
  }
288
314
 
289
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
+
290
321
  sections.push("\n## 📌 补充信息");
291
322
  for (const mem of clustered.standalone_memories) {
292
323
  const idTag = mem.id ? ` [id:${mem.id}]` : "";
293
324
  const relTag = mem.relations && mem.relations.length > 0
294
325
  ? ` [rel:${mem.relations.map((rel) => rel.target_id).join(",")}]`
295
326
  : "";
296
- const content = truncate(mem.content, maxContentLength);
327
+ const content = truncate(mem.content, standaloneMaxLen);
297
328
  sections.push(`-${idTag}${relTag} ${content}`);
298
329
  }
299
330
  }
@@ -316,7 +347,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
316
347
  const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
317
348
  const llmMaxEval = config.recall?.llmMaxEval ?? 15;
318
349
  const refineStrategy = config.recall?.refineStrategy ?? "balanced";
319
- const refineMediumChars = config.recall?.refineMediumChars ?? 200;
320
350
  const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
321
351
  const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
322
352
  const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
@@ -333,7 +363,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
333
363
  if (policy === "none") return;
334
364
 
335
365
  try {
336
- logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy, refineMediumChars });
366
+ logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy });
337
367
  const messages = sessionMessages.get(input.sessionID) ?? [];
338
368
  const userMessages = messages.filter((m) => m.role === "user");
339
369
 
@@ -399,7 +429,6 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
399
429
  phase2_multiplier: phase2Multiplier,
400
430
  llm_max_eval: llmMaxEval,
401
431
  refine_strategy: refineStrategy,
402
- refine_medium_chars: refineMediumChars,
403
432
  },
404
433
  directory || process.env.OMEM_PROJECT_DIR,
405
434
  );
@@ -494,20 +523,14 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
494
523
  if (budgetRemaining < 0) {
495
524
  logDebug("autoRecallHook budget overflow", { profileChars, maxContentChars, deficit: -budgetRemaining });
496
525
  }
497
- const itemCount = clustered
498
- ? (clustered.cluster_summaries.length + clustered.standalone_memories.length)
499
- : newResults.length;
500
- const dynamicMaxContentLength = itemCount > 0
501
- ? Math.min(maxContentLength, Math.max(MIN_ITEM_CONTENT_CHARS, Math.floor(budgetRemaining / itemCount)))
502
- : maxContentLength;
503
526
  logDebug("autoRecallHook budget", {
504
- maxContentChars, profileChars, budgetRemaining, itemCount,
505
- configuredMax: maxContentLength, dynamicMax: dynamicMaxContentLength
527
+ maxContentChars, profileChars, budgetRemaining,
528
+ configuredMax: maxContentLength,
506
529
  });
507
530
 
508
531
  const block = clustered
509
- ? buildClusteredContextBlock(clustered, dynamicMaxContentLength)
510
- : buildContextBlock(newResults, dynamicMaxContentLength);
532
+ ? buildClusteredContextBlock(clustered, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS)
533
+ : buildContextBlock(newResults, budgetRemaining, maxContentLength, MIN_ITEM_CONTENT_CHARS);
511
534
  if (block) {
512
535
  appendToSystem(output.system, block);
513
536
  appendToSystem(output.system, FETCH_POLICY);