@mingxy/cerebro 1.11.15 → 1.12.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.11.15",
3
+ "version": "1.12.0",
4
4
  "description": "Cerebro persistent memory plugin for OpenCode — auto-recall, auto-capture, 9 memory tools with clustering",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/schema.json CHANGED
@@ -96,6 +96,61 @@
96
96
  "default": 10,
97
97
  "minimum": 1,
98
98
  "maximum": 50
99
+ },
100
+ "fetchMultiplier": {
101
+ "type": "number",
102
+ "description": "Search breadth multiplier: fetch_limit = max_results * N",
103
+ "default": 3,
104
+ "minimum": 1,
105
+ "maximum": 10
106
+ },
107
+ "topkCapMultiplier": {
108
+ "type": "number",
109
+ "description": "Candidate cap multiplier: topk_cap = max_results * N",
110
+ "default": 2,
111
+ "minimum": 1,
112
+ "maximum": 10
113
+ },
114
+ "mmrJaccardThreshold": {
115
+ "type": "number",
116
+ "description": "Jaccard similarity threshold for MMR diversity penalty",
117
+ "default": 0.85,
118
+ "minimum": 0.0,
119
+ "maximum": 1.0
120
+ },
121
+ "mmrPenaltyFactor": {
122
+ "type": "number",
123
+ "description": "Score penalty factor for similar memories in MMR diversity",
124
+ "default": 0.5,
125
+ "minimum": 0.0,
126
+ "maximum": 1.0
127
+ },
128
+ "phase2Multiplier": {
129
+ "type": "number",
130
+ "description": "Phase2 global fallback search multiplier",
131
+ "default": 2,
132
+ "minimum": 1,
133
+ "maximum": 10
134
+ },
135
+ "llmMaxEval": {
136
+ "type": "number",
137
+ "description": "Maximum candidates sent to LLM for relevance evaluation",
138
+ "default": 15,
139
+ "minimum": 1,
140
+ "maximum": 50
141
+ },
142
+ "refineStrategy": {
143
+ "type": "string",
144
+ "description": "LLM refinement strategy: strict (high only), balanced (high+medium), loose (keep all)",
145
+ "enum": ["strict", "balanced", "loose"],
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
99
154
  }
100
155
  },
101
156
  "additionalProperties": false
package/src/client.ts CHANGED
@@ -55,6 +55,14 @@ export interface ClusteredRecallResult {
55
55
  standalone_memories: MemoryDto[];
56
56
  }
57
57
 
58
+ export interface DiscardedItem {
59
+ memory_id: string;
60
+ content: string;
61
+ score: number;
62
+ refine_relevance?: string;
63
+ refine_reasoning?: string;
64
+ }
65
+
58
66
  export interface ShouldRecallResponse {
59
67
  should_recall: boolean;
60
68
  query?: string;
@@ -62,8 +70,8 @@ export interface ShouldRecallResponse {
62
70
  similarity_score?: number;
63
71
  confidence?: number;
64
72
  memories?: SearchResult[];
73
+ discarded?: DiscardedItem[];
65
74
  clustered?: ClusteredRecallResult;
66
- event_id?: string;
67
75
  }
68
76
 
69
77
  export interface MemoryRelation {
@@ -332,6 +340,16 @@ export class CerebroClient {
332
340
  max_results?: number,
333
341
  project_tags?: string[],
334
342
  conversation_context?: string[],
343
+ recall_overrides?: {
344
+ fetch_multiplier?: number;
345
+ topk_cap_multiplier?: number;
346
+ mmr_jaccard_threshold?: number;
347
+ mmr_penalty_factor?: number;
348
+ phase2_multiplier?: number;
349
+ llm_max_eval?: number;
350
+ refine_strategy?: string;
351
+ refine_medium_chars?: number;
352
+ },
335
353
  ): Promise<ShouldRecallResponse | null> {
336
354
  const res = await this.post<ShouldRecallResponse>("/v1/should-recall", {
337
355
  query_text,
@@ -341,6 +359,7 @@ export class CerebroClient {
341
359
  max_results,
342
360
  project_tags,
343
361
  conversation_context,
362
+ ...recall_overrides,
344
363
  }, 20_000);
345
364
  return res;
346
365
  }
@@ -348,15 +367,42 @@ export class CerebroClient {
348
367
  async updateProfileInjected(
349
368
  event_id: string,
350
369
  profile_injected: boolean,
370
+ profile_content?: string,
351
371
  ): Promise<unknown | null> {
372
+ const body: Record<string, unknown> = { profile_injected };
373
+ if (profile_content !== undefined) {
374
+ body.profile_content = profile_content;
375
+ }
352
376
  const res = await this.patch(
353
377
  `/v1/recall-events/${event_id}/profile-injected`,
354
- { profile_injected },
378
+ body,
355
379
  10_000,
356
380
  );
357
381
  return res;
358
382
  }
359
383
 
384
+ async createRecallEvent(params: {
385
+ session_id: string;
386
+ recall_type?: string;
387
+ query_text: string;
388
+ max_score: number;
389
+ llm_confidence: number;
390
+ profile_injected: boolean;
391
+ kept_count: number;
392
+ discarded_count: number;
393
+ injected_count: number;
394
+ profile_content?: string;
395
+ items?: Array<{
396
+ memory_id: string;
397
+ score: number;
398
+ refine_relevance?: string;
399
+ refine_reasoning?: string;
400
+ is_kept: boolean;
401
+ }>;
402
+ }): Promise<{ ok: boolean; event_id?: string } | null> {
403
+ return this.post("/v1/recall-events", params, 10_000);
404
+ }
405
+
360
406
  async sessionIngest(
361
407
  messages: Array<{ role: string; content: string }>,
362
408
  sessionId?: string,
package/src/config.ts CHANGED
@@ -22,6 +22,14 @@ export interface OmemPluginConfig {
22
22
  recall: {
23
23
  similarityThreshold: number;
24
24
  maxRecallResults: number;
25
+ fetchMultiplier: number;
26
+ topkCapMultiplier: number;
27
+ mmrJaccardThreshold: number;
28
+ mmrPenaltyFactor: number;
29
+ phase2Multiplier: number;
30
+ llmMaxEval: number;
31
+ refineStrategy: "strict" | "balanced" | "loose";
32
+ refineMediumChars: number;
25
33
  };
26
34
  logging: {
27
35
  logEnabled: boolean;
@@ -55,6 +63,14 @@ const DEFAULTS: OmemPluginConfig = {
55
63
  recall: {
56
64
  similarityThreshold: 0.4,
57
65
  maxRecallResults: 10,
66
+ fetchMultiplier: 3,
67
+ topkCapMultiplier: 2,
68
+ mmrJaccardThreshold: 0.85,
69
+ mmrPenaltyFactor: 0.5,
70
+ phase2Multiplier: 2,
71
+ llmMaxEval: 15,
72
+ refineStrategy: "balanced",
73
+ refineMediumChars: 200,
58
74
  },
59
75
  logging: {
60
76
  logEnabled: true,
@@ -111,6 +127,14 @@ function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
111
127
  recall: {
112
128
  similarityThreshold: flat.similarityThreshold ?? DEFAULTS.recall.similarityThreshold,
113
129
  maxRecallResults: flat.maxRecallResults ?? DEFAULTS.recall.maxRecallResults,
130
+ fetchMultiplier: DEFAULTS.recall.fetchMultiplier,
131
+ topkCapMultiplier: DEFAULTS.recall.topkCapMultiplier,
132
+ mmrJaccardThreshold: DEFAULTS.recall.mmrJaccardThreshold,
133
+ mmrPenaltyFactor: DEFAULTS.recall.mmrPenaltyFactor,
134
+ phase2Multiplier: DEFAULTS.recall.phase2Multiplier,
135
+ llmMaxEval: DEFAULTS.recall.llmMaxEval,
136
+ refineStrategy: DEFAULTS.recall.refineStrategy,
137
+ refineMediumChars: DEFAULTS.recall.refineMediumChars,
114
138
  },
115
139
  logging: {
116
140
  logEnabled: flat.logEnabled ?? DEFAULTS.logging.logEnabled,
package/src/hooks.ts CHANGED
@@ -299,6 +299,14 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
299
299
  export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}, getAgentName?: () => string) {
300
300
  const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
301
301
  const maxRecallResults = config.recall?.maxRecallResults ?? 10;
302
+ const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
303
+ const topkCapMultiplier = config.recall?.topkCapMultiplier ?? 2;
304
+ const mmrJaccardThreshold = config.recall?.mmrJaccardThreshold ?? 0.85;
305
+ const mmrPenaltyFactor = config.recall?.mmrPenaltyFactor ?? 0.5;
306
+ const phase2Multiplier = config.recall?.phase2Multiplier ?? 2;
307
+ const llmMaxEval = config.recall?.llmMaxEval ?? 15;
308
+ const refineStrategy = config.recall?.refineStrategy ?? "balanced";
309
+ const refineMediumChars = config.recall?.refineMediumChars ?? 200;
302
310
  const maxContentLength = Math.max(MIN_CONTENT_LENGTH, config.content?.maxContentLength ?? 500);
303
311
  const maxContentChars = Math.max(MIN_CONTENT_CHARS, config.content?.maxContentChars ?? 30000);
304
312
  const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
@@ -348,13 +356,23 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
348
356
  similarityThreshold, maxRecallResults,
349
357
  projectTags.length > 0 ? projectTags : undefined,
350
358
  conversationContext && conversationContext.length > 0 ? conversationContext : undefined,
359
+ {
360
+ fetch_multiplier: fetchMultiplier,
361
+ topk_cap_multiplier: topkCapMultiplier,
362
+ mmr_jaccard_threshold: mmrJaccardThreshold,
363
+ mmr_penalty_factor: mmrPenaltyFactor,
364
+ phase2_multiplier: phase2Multiplier,
365
+ llm_max_eval: llmMaxEval,
366
+ refine_strategy: refineStrategy,
367
+ refine_medium_chars: refineMediumChars,
368
+ },
351
369
  );
352
370
 
353
371
  if (!shouldRecallRes) {
354
372
  showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
355
373
  return;
356
374
  }
357
- logDebug("autoRecallHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, clustered: !!shouldRecallRes.clustered });
375
+ logDebug("autoRecallHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, discardedCount: shouldRecallRes.discarded?.length ?? 0, clustered: !!shouldRecallRes.clustered });
358
376
 
359
377
  const profile = await client.getProfile();
360
378
  let profileInjected = false;
@@ -398,10 +416,56 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
398
416
  }
399
417
  }
400
418
 
401
- if (!shouldRecallRes.should_recall) {
402
- if (profileInjected && shouldRecallRes?.event_id) {
403
- await client.updateProfileInjected(shouldRecallRes.event_id, true).catch(() => {});
419
+ const storedMemoryIds = shouldRecallRes.memories?.map((r) => r.memory.id) ?? [];
420
+ const storedDiscardedIds = shouldRecallRes.discarded?.map((d) => d.memory_id) ?? [];
421
+ const maxScore = storedMemoryIds.length > 0
422
+ ? Math.max(...(shouldRecallRes.memories?.map((r) => r.score) ?? [0]))
423
+ : 0;
424
+
425
+ const createEventAndReturn = async (
426
+ injectedCount: number,
427
+ keptCount: number,
428
+ discardedCount: number,
429
+ ): Promise<string | undefined> => {
430
+ try {
431
+ const items = [
432
+ ...(shouldRecallRes.memories?.map((r) => ({
433
+ memory_id: r.memory.id,
434
+ score: r.score,
435
+ refine_relevance: r.refine_relevance,
436
+ refine_reasoning: r.refine_reasoning,
437
+ is_kept: true,
438
+ })) ?? []),
439
+ ...(shouldRecallRes.discarded?.map((d) => ({
440
+ memory_id: d.memory_id,
441
+ score: d.score,
442
+ refine_relevance: d.refine_relevance,
443
+ refine_reasoning: d.refine_reasoning,
444
+ is_kept: false,
445
+ })) ?? []),
446
+ ];
447
+ const result = await client.createRecallEvent({
448
+ session_id: input.sessionID!,
449
+ recall_type: "auto",
450
+ query_text,
451
+ max_score: maxScore,
452
+ llm_confidence: shouldRecallRes.confidence ?? 0,
453
+ profile_injected: profileInjected,
454
+ kept_count: keptCount,
455
+ discarded_count: discardedCount,
456
+ injected_count: injectedCount,
457
+ profile_content: profileInjected && profileBlock ? profileBlock : undefined,
458
+ items: items.length > 0 ? items : undefined,
459
+ });
460
+ return result?.event_id;
461
+ } catch (e) {
462
+ logErr("autoRecallHook createRecallEvent failed", { error: String(e) });
463
+ return undefined;
404
464
  }
465
+ };
466
+
467
+ if (!shouldRecallRes.should_recall) {
468
+ await createEventAndReturn(0, 0, storedDiscardedIds.length);
405
469
  if (profileInjected && isFirstInjection) {
406
470
  showToast(tui, "👨 Profile Injected", `${profileCountText} · no memory recall needed`, "success", toastDelayMs);
407
471
  }
@@ -415,9 +479,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
415
479
  const newResults = results.filter((r) => !existingIds.has(r.memory.id));
416
480
  logDebug("autoRecallHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
417
481
  if (newResults.length === 0) {
418
- if (profileInjected && shouldRecallRes?.event_id) {
419
- await client.updateProfileInjected(shouldRecallRes.event_id, true).catch(() => {});
420
- }
482
+ await createEventAndReturn(0, storedMemoryIds.length, storedDiscardedIds.length);
421
483
  if (profileInjected && isFirstInjection) {
422
484
  showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
423
485
  }
@@ -453,9 +515,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
453
515
  injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
454
516
  logDebug("autoRecallHook injection complete", { newIds: newIds.length, clustered: !!clustered });
455
517
 
456
- if (profileInjected && shouldRecallRes?.event_id) {
457
- await client.updateProfileInjected(shouldRecallRes.event_id, true).catch(() => {});
458
- }
518
+ await createEventAndReturn(newResults.length, storedMemoryIds.length, storedDiscardedIds.length);
459
519
 
460
520
  const memDynamic = newResults.filter((r) => r.memory.memory_type === "fact" || r.memory.memory_type === "event").length;
461
521
  const memStatic = newResults.filter((r) => r.memory.memory_type === "pinned" || r.memory.memory_type === "preference").length;