@mingxy/cerebro 1.11.15 → 1.12.1

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.1",
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;
@@ -315,7 +323,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
315
323
  if (policy === "none") return;
316
324
 
317
325
  try {
318
- logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy });
326
+ logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy, similarityThreshold, maxRecallResults, fetchMultiplier, topkCapMultiplier, mmrJaccardThreshold, mmrPenaltyFactor, phase2Multiplier, llmMaxEval, refineStrategy, refineMediumChars });
319
327
  const messages = sessionMessages.get(input.sessionID) ?? [];
320
328
  const userMessages = messages.filter((m) => m.role === "user");
321
329
 
@@ -348,26 +356,36 @@ 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;
361
379
  let profileCountText = "";
362
380
  let profileBlock = "";
363
381
  const lastInjected = profileInjectedSessions.get(input.sessionID);
364
- const ttlExpired = !lastInjected || (Date.now() - lastInjected > 5 * 60 * 1000);
382
+ const ttlExpired = !lastInjected || (Date.now() - lastInjected > 30 * 60 * 1000);
365
383
  const isFirstInjection = !lastInjected;
366
384
  if (profile && ttlExpired) {
367
385
  const prefs = ((profile as any)?.static_facts ?? [])
368
386
  .filter((sf: any) => {
369
387
  const t: string[] = sf.tags ?? [];
370
- return t.includes("preferences") || t.includes("preference_extract") || t.some((tag: string) => tag.includes("偏好"));
388
+ return t.includes("preferences");
371
389
  })
372
390
  .map((sf: any) => sf.l2_content ?? sf.content ?? "")
373
391
  .filter(Boolean);
@@ -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;
package/src/tools.ts CHANGED
@@ -52,17 +52,17 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
52
52
  "Do NOT overuse 'private' for normal work notes — default 'global' is correct for most cases."
53
53
  ),
54
54
  category: tool.schema
55
- .string()
55
+ .enum(["cases", "preferences", "entities", "events", "profile", "patterns"])
56
56
  .optional()
57
57
  .describe(
58
- "MUST be one of (choose the BEST fit): " +
59
- "'cases' (default) = work records, bug fixes, architecture decisions, implementation notes, meeting conclusions; " +
60
- "'preferences' = user likes/dislikes, coding style preferences, tool choices (e.g. 'prefers Vim over VSCode'); " +
61
- "'entities' = projects, tools, people, concepts — defining what something IS (e.g. 'omem-server: Rust memory backend using LanceDB'); " +
58
+ "Memory category. MUST be one of these exact values (lowercase): " +
59
+ "'cases' (default) = work records, bug fixes, architecture decisions; " +
60
+ "'preferences' = user likes/dislikes, coding style, tool choices; " +
61
+ "'entities' = projects, tools, people, concepts; " +
62
62
  "'events' = time-bound milestones (deployments, releases, incidents); " +
63
63
  "'profile' = user identity traits (role, skills, team membership); " +
64
- "'patterns' = workflows, methodologies, best practices, recurring solutions. " +
65
- "When in doubt, use 'cases'."
64
+ "'patterns' = workflows, methodologies, best practices. " +
65
+ "When in doubt, omit this field (defaults to 'cases')."
66
66
  ),
67
67
  },
68
68
  async execute(args) {