@mingxy/cerebro 1.10.6 → 1.10.8

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/config.ts CHANGED
@@ -1,104 +1,207 @@
1
- import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- export interface OmemPluginConfig {
6
- // Connection
7
- apiUrl: string;
8
- apiKey: string;
9
- // Timeouts (milliseconds)
10
- requestTimeoutMs: number;
11
- // Content limits
12
- maxQueryLength: number;
13
- maxContentChars: number;
14
- maxContentLength: number;
15
- // Auto capture
16
- autoCaptureThreshold: number;
17
- ingestMode: "smart" | "raw";
18
- // Recall settings
19
- similarityThreshold: number;
20
- maxRecallResults: number;
21
- // UI settings
22
- toastDelayMs: number;
23
- // Logging
24
- logEnabled: boolean;
25
- logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
26
- logDir: string;
27
- }
28
-
29
- const DEFAULTS: OmemPluginConfig = {
30
- apiUrl: "https://www.mengxy.cc",
31
- apiKey: "",
32
- requestTimeoutMs: 15000,
33
- maxQueryLength: 200,
34
- maxContentChars: 30000,
35
- maxContentLength: 500,
36
- autoCaptureThreshold: 5,
37
- ingestMode: "smart",
38
- similarityThreshold: 0.4,
39
- maxRecallResults: 10,
40
- toastDelayMs: 7000,
41
- logEnabled: true,
42
- logLevel: "INFO",
43
- logDir: join(homedir(), ".config", "cerebro"),
44
- };
45
-
46
- export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPluginConfig {
47
- const config: Partial<OmemPluginConfig> = { ...DEFAULTS };
48
-
49
- // Try loading from config file
50
- try {
51
- const cfgPath = join(homedir(), ".config", "cerebro", "config.json");
52
- const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
53
-
54
- if (cfg.apiUrl) config.apiUrl = cfg.apiUrl;
55
- if (cfg.apiKey) config.apiKey = cfg.apiKey;
56
- if (typeof cfg.requestTimeoutMs === "number") config.requestTimeoutMs = cfg.requestTimeoutMs;
57
- if (typeof cfg.maxQueryLength === "number") config.maxQueryLength = cfg.maxQueryLength;
58
- if (typeof cfg.maxContentChars === "number") config.maxContentChars = cfg.maxContentChars;
59
- if (typeof cfg.maxContentLength === "number") config.maxContentLength = cfg.maxContentLength;
60
- if (typeof cfg.autoCaptureThreshold === "number") config.autoCaptureThreshold = cfg.autoCaptureThreshold;
61
- if (cfg.ingestMode === "raw" || cfg.ingestMode === "smart") config.ingestMode = cfg.ingestMode;
62
- if (typeof cfg.similarityThreshold === "number") config.similarityThreshold = cfg.similarityThreshold;
63
- if (typeof cfg.maxRecallResults === "number") config.maxRecallResults = cfg.maxRecallResults;
64
- if (typeof cfg.toastDelayMs === "number") config.toastDelayMs = cfg.toastDelayMs;
65
- if (typeof cfg.logEnabled === "boolean") config.logEnabled = cfg.logEnabled;
66
- if (cfg.logLevel === "DEBUG" || cfg.logLevel === "INFO" || cfg.logLevel === "WARN" || cfg.logLevel === "ERROR") config.logLevel = cfg.logLevel;
67
- if (typeof cfg.logDir === "string" && cfg.logDir) config.logDir = cfg.logDir;
68
- } catch {
69
- // Config file doesn't exist or is invalid, use defaults
70
- }
71
-
72
- // Apply environment variable overrides
73
- if (process.env.OMEM_API_URL) config.apiUrl = process.env.OMEM_API_URL;
74
- if (process.env.OMEM_API_KEY) config.apiKey = process.env.OMEM_API_KEY;
75
- if (process.env.OMEM_REQUEST_TIMEOUT_MS) {
76
- config.requestTimeoutMs = parseInt(process.env.OMEM_REQUEST_TIMEOUT_MS, 10) || DEFAULTS.requestTimeoutMs;
77
- }
78
- if (process.env.OMEM_AUTO_CAPTURE_THRESHOLD) {
79
- config.autoCaptureThreshold = parseInt(process.env.OMEM_AUTO_CAPTURE_THRESHOLD, 10) || DEFAULTS.autoCaptureThreshold;
80
- }
81
- if (process.env.OMEM_INGEST_MODE === "raw" || process.env.OMEM_INGEST_MODE === "smart") {
82
- config.ingestMode = process.env.OMEM_INGEST_MODE;
83
- }
84
- if (process.env.OMEM_SIMILARITY_THRESHOLD) {
85
- config.similarityThreshold = parseFloat(process.env.OMEM_SIMILARITY_THRESHOLD) || DEFAULTS.similarityThreshold;
86
- }
87
- if (process.env.OMEM_MAX_RECALL_RESULTS) {
88
- config.maxRecallResults = parseInt(process.env.OMEM_MAX_RECALL_RESULTS, 10) || DEFAULTS.maxRecallResults;
89
- }
90
-
91
- // Apply explicit overrides (from opencode.json)
92
- if (overrides) {
93
- Object.assign(config, overrides);
94
- }
95
-
96
- // Expand ~ to home directory in logDir
97
- if (config.logDir?.startsWith("~")) {
98
- config.logDir = config.logDir.replace(/^~/, homedir());
99
- }
100
-
101
- return config as OmemPluginConfig;
102
- }
103
-
104
- export { DEFAULTS };
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ // ── Nested config interface ──────────────────────────────────────────
6
+
7
+ export interface OmemPluginConfig {
8
+ connection: {
9
+ apiUrl: string;
10
+ apiKey: string;
11
+ requestTimeoutMs: number;
12
+ };
13
+ content: {
14
+ maxQueryLength: number;
15
+ maxContentChars: number;
16
+ maxContentLength: number;
17
+ };
18
+ ingest: {
19
+ autoCaptureThreshold: number;
20
+ ingestMode: "smart" | "raw";
21
+ };
22
+ recall: {
23
+ similarityThreshold: number;
24
+ maxRecallResults: number;
25
+ };
26
+ logging: {
27
+ logEnabled: boolean;
28
+ logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
29
+ logDir: string;
30
+ };
31
+ ui: {
32
+ toastDelayMs: number;
33
+ };
34
+ agentMemoryPolicy?: Record<string, "none" | "readonly" | "readwrite">;
35
+ defaultPolicy?: "none" | "readonly" | "readwrite";
36
+ }
37
+
38
+ // ── Defaults ─────────────────────────────────────────────────────────
39
+
40
+ const DEFAULTS: OmemPluginConfig = {
41
+ connection: {
42
+ apiUrl: "https://www.mengxy.cc",
43
+ apiKey: "",
44
+ requestTimeoutMs: 15000,
45
+ },
46
+ content: {
47
+ maxQueryLength: 200,
48
+ maxContentChars: 30000,
49
+ maxContentLength: 500,
50
+ },
51
+ ingest: {
52
+ autoCaptureThreshold: 5,
53
+ ingestMode: "smart",
54
+ },
55
+ recall: {
56
+ similarityThreshold: 0.4,
57
+ maxRecallResults: 10,
58
+ },
59
+ logging: {
60
+ logEnabled: true,
61
+ logLevel: "INFO",
62
+ logDir: join(homedir(), ".config", "cerebro"),
63
+ },
64
+ ui: {
65
+ toastDelayMs: 7000,
66
+ },
67
+ };
68
+
69
+ // ── Flat-to-nested migration ─────────────────────────────────────────
70
+
71
+ /** Shape of legacy flat config (pre-nesting). */
72
+ interface FlatConfig {
73
+ apiUrl?: string;
74
+ apiKey?: string;
75
+ requestTimeoutMs?: number;
76
+ maxQueryLength?: number;
77
+ maxContentChars?: number;
78
+ maxContentLength?: number;
79
+ autoCaptureThreshold?: number;
80
+ ingestMode?: "smart" | "raw";
81
+ similarityThreshold?: number;
82
+ maxRecallResults?: number;
83
+ toastDelayMs?: number;
84
+ logEnabled?: boolean;
85
+ logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR";
86
+ logDir?: string;
87
+ // Nested fields that would indicate new format
88
+ connection?: unknown;
89
+ }
90
+
91
+ function isFlatConfig(cfg: Record<string, unknown>): boolean {
92
+ return "apiUrl" in cfg && !("connection" in cfg);
93
+ }
94
+
95
+ function migrateFlatToNested(flat: FlatConfig): OmemPluginConfig {
96
+ return {
97
+ connection: {
98
+ apiUrl: flat.apiUrl ?? DEFAULTS.connection.apiUrl,
99
+ apiKey: flat.apiKey ?? DEFAULTS.connection.apiKey,
100
+ requestTimeoutMs: flat.requestTimeoutMs ?? DEFAULTS.connection.requestTimeoutMs,
101
+ },
102
+ content: {
103
+ maxQueryLength: flat.maxQueryLength ?? DEFAULTS.content.maxQueryLength,
104
+ maxContentChars: flat.maxContentChars ?? DEFAULTS.content.maxContentChars,
105
+ maxContentLength: flat.maxContentLength ?? DEFAULTS.content.maxContentLength,
106
+ },
107
+ ingest: {
108
+ autoCaptureThreshold: flat.autoCaptureThreshold ?? DEFAULTS.ingest.autoCaptureThreshold,
109
+ ingestMode: flat.ingestMode ?? DEFAULTS.ingest.ingestMode,
110
+ },
111
+ recall: {
112
+ similarityThreshold: flat.similarityThreshold ?? DEFAULTS.recall.similarityThreshold,
113
+ maxRecallResults: flat.maxRecallResults ?? DEFAULTS.recall.maxRecallResults,
114
+ },
115
+ logging: {
116
+ logEnabled: flat.logEnabled ?? DEFAULTS.logging.logEnabled,
117
+ logLevel: flat.logLevel ?? DEFAULTS.logging.logLevel,
118
+ logDir: flat.logDir ?? DEFAULTS.logging.logDir,
119
+ },
120
+ ui: {
121
+ toastDelayMs: flat.toastDelayMs ?? DEFAULTS.ui.toastDelayMs,
122
+ },
123
+ };
124
+ }
125
+
126
+ // ── Helpers ──────────────────────────────────────────────────────────
127
+
128
+ type IngestMode = "smart" | "raw";
129
+ const INGEST_MODES: ReadonlySet<string> = new Set<IngestMode>(["smart", "raw"]);
130
+
131
+ function deepMerge(base: OmemPluginConfig, overrides: Partial<OmemPluginConfig>): OmemPluginConfig {
132
+ const result: OmemPluginConfig = {
133
+ connection: { ...base.connection, ...overrides.connection },
134
+ content: { ...base.content, ...overrides.content },
135
+ ingest: { ...base.ingest, ...overrides.ingest },
136
+ recall: { ...base.recall, ...overrides.recall },
137
+ logging: { ...base.logging, ...overrides.logging },
138
+ ui: { ...base.ui, ...overrides.ui },
139
+ };
140
+ if (overrides.agentMemoryPolicy) result.agentMemoryPolicy = overrides.agentMemoryPolicy;
141
+ if (overrides.defaultPolicy) result.defaultPolicy = overrides.defaultPolicy;
142
+ return result;
143
+ }
144
+
145
+ // ── Load config ──────────────────────────────────────────────────────
146
+
147
+ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPluginConfig {
148
+ let config: OmemPluginConfig = structuredClone(DEFAULTS);
149
+
150
+ // Try loading from config file
151
+ try {
152
+ const cfgPath = join(homedir(), ".config", "cerebro", "config.json");
153
+ const raw = JSON.parse(readFileSync(cfgPath, "utf-8")) as Record<string, unknown>;
154
+
155
+ // Auto-migrate flat format
156
+ const parsed: OmemPluginConfig = isFlatConfig(raw) ? migrateFlatToNested(raw as FlatConfig) : raw as unknown as OmemPluginConfig;
157
+
158
+ // Merge nested groups with defaults for safety
159
+ config = deepMerge(config, parsed);
160
+ } catch {
161
+ // Config file doesn't exist or is invalid, use defaults
162
+ }
163
+
164
+ // Apply environment variable overrides (flat OMEM_* → nested paths)
165
+ if (process.env.OMEM_API_URL) config.connection.apiUrl = process.env.OMEM_API_URL;
166
+ if (process.env.OMEM_API_KEY) config.connection.apiKey = process.env.OMEM_API_KEY;
167
+ if (process.env.OMEM_REQUEST_TIMEOUT_MS) {
168
+ config.connection.requestTimeoutMs = parseInt(process.env.OMEM_REQUEST_TIMEOUT_MS, 10) || DEFAULTS.connection.requestTimeoutMs;
169
+ }
170
+ if (process.env.OMEM_AUTO_CAPTURE_THRESHOLD) {
171
+ config.ingest.autoCaptureThreshold = parseInt(process.env.OMEM_AUTO_CAPTURE_THRESHOLD, 10) || DEFAULTS.ingest.autoCaptureThreshold;
172
+ }
173
+ if (INGEST_MODES.has(process.env.OMEM_INGEST_MODE ?? "")) {
174
+ config.ingest.ingestMode = process.env.OMEM_INGEST_MODE as IngestMode;
175
+ }
176
+ if (process.env.OMEM_SIMILARITY_THRESHOLD) {
177
+ config.recall.similarityThreshold = parseFloat(process.env.OMEM_SIMILARITY_THRESHOLD) || DEFAULTS.recall.similarityThreshold;
178
+ }
179
+ if (process.env.OMEM_MAX_RECALL_RESULTS) {
180
+ config.recall.maxRecallResults = parseInt(process.env.OMEM_MAX_RECALL_RESULTS, 10) || DEFAULTS.recall.maxRecallResults;
181
+ }
182
+
183
+ // Apply explicit overrides (from opencode.json)
184
+ if (overrides) {
185
+ config = deepMerge(config, overrides);
186
+ }
187
+
188
+ // Expand ~ to home directory in logDir
189
+ if (config.logging.logDir?.startsWith("~")) {
190
+ config.logging.logDir = config.logging.logDir.replace(/^~/, homedir());
191
+ }
192
+
193
+ return config;
194
+ }
195
+
196
+ // ── Agent policy resolver ────────────────────────────────────────────
197
+
198
+ export type AgentPolicy = "none" | "readonly" | "readwrite";
199
+
200
+ export function resolveAgentPolicy(
201
+ agentName: string,
202
+ config: Partial<OmemPluginConfig>,
203
+ ): AgentPolicy {
204
+ return config.agentMemoryPolicy?.[agentName] ?? config.defaultPolicy ?? "readwrite";
205
+ }
206
+
207
+ export { DEFAULTS };
package/src/hooks.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Model, UserMessage, Part } from "@opencode-ai/sdk";
2
- import type { OmemClient, SearchResult } from "./client.js";
3
- import type { OmemPluginConfig } from "./config.js";
2
+ import type { CerebroClient, SearchResult } from "./client.js";
3
+ import { type OmemPluginConfig, resolveAgentPolicy } from "./config.js";
4
4
  import { detectKeyword, KEYWORD_NUDGE } from "./keywords.js";
5
- import { logDebug, logError as logErr } from "./logger.js";
5
+ import { logDebug, logInfo, logError as logErr } from "./logger.js";
6
6
  import { readFile } from "node:fs/promises";
7
7
 
8
8
  const projectNameCache = new Map<string, string>();
@@ -182,12 +182,12 @@ function buildContextBlock(results: SearchResult[], maxContentLength: number = 5
182
182
  }
183
183
 
184
184
  return [
185
- "<omem-context>",
185
+ "<cerebro-context>",
186
186
  "Treat every memory below as historical context only.",
187
187
  "Do not repeat these memories verbatim unless asked.",
188
188
  "",
189
189
  ...sections,
190
- "</omem-context>",
190
+ "</cerebro-context>",
191
191
  ].join("\n");
192
192
  }
193
193
 
@@ -220,20 +220,20 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
220
220
  }
221
221
 
222
222
  return [
223
- "<omem-context>",
223
+ "<cerebro-context>",
224
224
  "Treat every memory below as historical context only.",
225
225
  "Do not repeat these memories verbatim unless asked.",
226
226
  "",
227
227
  ...sections,
228
- "</omem-context>",
228
+ "</cerebro-context>",
229
229
  ].join("\n");
230
230
  }
231
231
 
232
- export function autoRecallHook(client: OmemClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}) {
233
- const similarityThreshold = config.similarityThreshold ?? 0.6;
234
- const maxRecallResults = config.maxRecallResults ?? 10;
235
- const maxContentLength = config.maxContentLength ?? 500;
236
- const toastDelayMs = config.toastDelayMs ?? 7000;
232
+ export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}) {
233
+ const similarityThreshold = config.recall?.similarityThreshold ?? 0.6;
234
+ const maxRecallResults = config.recall?.maxRecallResults ?? 10;
235
+ const maxContentLength = config.content?.maxContentLength ?? 500;
236
+ const toastDelayMs = config.ui?.toastDelayMs ?? 7000;
237
237
 
238
238
  return async (
239
239
  input: { sessionID?: string; model: Model },
@@ -241,7 +241,13 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
241
241
  ) => {
242
242
  if (!input.sessionID) return;
243
243
 
244
+ // 5a: agent memory policy check — skip recall entirely for 'none' agents
245
+ const agentId = process.env.OMEM_AGENT_ID || "opencode";
246
+ const policy = resolveAgentPolicy(agentId, config);
247
+ if (policy === "none") return;
248
+
244
249
  try {
250
+ logDebug("autoRecallHook start", { sessionId: input.sessionID, agentId, policy });
245
251
  const messages = sessionMessages.get(input.sessionID) ?? [];
246
252
  const userMessages = messages.filter((m) => m.role === "user");
247
253
  const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
@@ -255,15 +261,16 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
255
261
  showToast(tui, "🧠 Cerebro Service Unavailable", "Unable to reach memory API · check connection", "error", toastDelayMs);
256
262
  return;
257
263
  }
264
+ logDebug("autoRecallHook shouldRecall result", { shouldRecall: shouldRecallRes.should_recall, confidence: shouldRecallRes.confidence, memCount: shouldRecallRes.memories?.length ?? 0, clustered: !!shouldRecallRes.clustered });
258
265
 
259
266
  const profile = await client.getProfile();
260
267
  let profileInjected = false;
261
268
  let profileCountText = "";
262
269
  if (profile && !profileInjectedSessions.has(input.sessionID)) {
263
270
  const profileBlock = [
264
- "<omem-profile>",
271
+ "<cerebro-profile>",
265
272
  JSON.stringify(profile, null, 2),
266
- "</omem-profile>",
273
+ "</cerebro-profile>",
267
274
  ].join("\n");
268
275
  output.system.push(profileBlock);
269
276
  profileInjected = true;
@@ -272,6 +279,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
272
279
  const dynamicCount = p?.dynamic_context?.length ?? 0;
273
280
  const staticCount = p?.static_facts?.length ?? 0;
274
281
  profileCountText = `Dynamic(${dynamicCount}) · Static(${staticCount})`;
282
+ logDebug("autoRecallHook profile injected", { dynamicCount, staticCount });
275
283
  }
276
284
 
277
285
  if (!shouldRecallRes.should_recall) {
@@ -286,6 +294,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
286
294
 
287
295
  const existingIds = injectedMemoryIds.get(input.sessionID) ?? new Set<string>();
288
296
  const newResults = results.filter((r) => !existingIds.has(r.memory.id));
297
+ logDebug("autoRecallHook dedup", { totalResults: results.length, existingCount: existingIds.size, newCount: newResults.length });
289
298
  if (newResults.length === 0) {
290
299
  if (profileInjected) {
291
300
  showToast(tui, "👨 Profile Injected", `${profileCountText} · all memories already injected`, "success", toastDelayMs);
@@ -302,6 +311,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
302
311
 
303
312
  const newIds = newResults.map((r) => r.memory.id);
304
313
  injectedMemoryIds.set(input.sessionID, new Set([...existingIds, ...newIds]));
314
+ logDebug("autoRecallHook injection complete", { newIds: newIds.length, clustered: !!clustered });
305
315
 
306
316
  const recordResult = await client.recordSessionRecall(
307
317
  input.sessionID,
@@ -355,9 +365,9 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
355
365
  }
356
366
  } catch (err) {
357
367
  const errMsg = err instanceof Error ? err.message : String(err);
358
- if (errMsg.includes("[omem]")) {
368
+ if (errMsg.includes("[cerebro]")) {
359
369
  // Server returned error (500, etc.) with details
360
- const cleanMsg = errMsg.replace(/^\[omem\]\s*/, "");
370
+ const cleanMsg = errMsg.replace(/^\[cerebro\]\s*/, "");
361
371
  if (cleanMsg.startsWith("500")) {
362
372
  showToast(tui, "🧠 Cerebro Server Error", cleanMsg.substring(0, 200), "error");
363
373
  } else if (cleanMsg.includes("timed out")) {
@@ -374,7 +384,8 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
374
384
  };
375
385
  }
376
386
 
377
- export function keywordDetectionHook(_client: OmemClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart") {
387
+ export function keywordDetectionHook(_client: CerebroClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart", config: Partial<OmemPluginConfig> = {}, agentId?: string) {
388
+ const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
378
389
  return async (
379
390
  input: { sessionID: string; messageID?: string },
380
391
  output: { message: UserMessage; parts: Part[] },
@@ -392,6 +403,12 @@ export function keywordDetectionHook(_client: OmemClient, _containerTags: string
392
403
 
393
404
  if (detectKeyword(textContent)) {
394
405
  keywordDetectedSessions.add(input.sessionID);
406
+ logDebug("keywordDetectionHook triggered", { sessionId: input.sessionID });
407
+ }
408
+
409
+ const policy = resolveAgentPolicy(effectiveAgentId, config);
410
+ if (policy === "none") {
411
+ return;
395
412
  }
396
413
 
397
414
  if (!sessionMessages.has(input.sessionID)) {
@@ -403,55 +420,69 @@ export function keywordDetectionHook(_client: OmemClient, _containerTags: string
403
420
  });
404
421
 
405
422
  const messages = sessionMessages.get(input.sessionID)!;
406
- // Ingest is now handled by sessionIdleHook (session.idle → sessionIngest API).
407
- // This hook only collects messages and detects keywords for recall.
408
423
  if (messages.length >= threshold) {
409
424
  // Threshold reached — messages will be processed on next session.idle
410
425
  }
411
426
  };
412
427
  }
413
428
 
414
- export function compactingHook(client: OmemClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean, getMainSessionId?: () => string | undefined, sdkClient?: any) {
429
+ export function compactingHook(client: CerebroClient, containerTags: string[], tui: any, ingestMode: "smart" | "raw" = "smart", isAutoStoreEnabled?: (sessionId: string | undefined) => boolean, getMainSessionId?: () => string | undefined, sdkClient?: any, config: Partial<OmemPluginConfig> = {}, agentId?: string) {
430
+ const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
415
431
  return async (
416
432
  input: { sessionID?: string },
417
433
  output: { context: string[]; prompt?: string },
418
434
  ) => {
435
+ // Search (read) always runs — even readonly agents need context during compacting
436
+ try {
437
+ const results = await client.searchMemories("*", 20, undefined, containerTags);
438
+ const block = buildContextBlock(results);
439
+ if (block) {
440
+ output.context.push(block);
441
+ }
442
+ } catch {
443
+ }
444
+
445
+ // Policy gate: only readwrite agents can write memories
446
+ const policy = resolveAgentPolicy(effectiveAgentId, config);
447
+ if (policy !== "readwrite") {
448
+ logInfo("compactingHook blocked by policy", { agentId: effectiveAgentId, policy });
449
+ if (input.sessionID) sessionMessages.delete(input.sessionID);
450
+ return;
451
+ }
452
+
419
453
  if (input.sessionID && sessionMessages.has(input.sessionID)) {
420
454
  if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
421
455
  sessionMessages.delete(input.sessionID);
422
456
  } else {
423
457
  const messages = sessionMessages.get(input.sessionID)!;
424
458
  if (messages.length > 0) {
459
+
425
460
  // Use main session ID for sub-agent sessions so memories merge into the main session
426
461
  const effectiveSessionId = (getMainSessionId?.() || input.sessionID);
427
- const isSubAgent = getMainSessionId?.() && input.sessionID !== getMainSessionId();
428
462
 
429
463
  // Detect project name from session info
430
464
  let projectName: string | undefined;
431
465
  try {
432
466
  if (sdkClient && input.sessionID) {
433
467
  const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
434
- logDebug("compactingHook sessionInfo", { sessionInfo: JSON.stringify(sessionInfo) });
435
468
  logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
436
469
  projectName = sessionInfo?.data?.directory
437
470
  ? await detectProjectName(sessionInfo.data.directory)
438
471
  : undefined;
439
- logDebug("compactingHook projectName", { projectName: String(projectName) });
440
472
  }
441
473
  } catch (e) {
442
474
  logErr("compactingHook detectProjectName failed", { error: String(e) });
443
475
  }
444
476
 
445
477
  try {
446
- logDebug("compactingHook ingestMessages called", { msgCount: messages.length, projectName: String(projectName), sessionId: effectiveSessionId });
478
+ logInfo("compactingHook ingestMessages called", { msgCount: messages.length, sessionId: effectiveSessionId });
447
479
  const result = await client.ingestMessages(messages, {
448
480
  mode: ingestMode,
449
481
  tags: [...containerTags, "auto-capture"],
450
482
  sessionId: effectiveSessionId,
451
- parentSessionId: isSubAgent ? getMainSessionId?.() : undefined,
452
483
  projectName: projectName,
453
484
  });
454
- logDebug("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
485
+ logInfo("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
455
486
  if (result === null) {
456
487
  showToast(tui, "🔴 Archive Failed", "Session archive blocked · check spiritual realm status", "error");
457
488
  } else {
@@ -465,15 +496,6 @@ export function compactingHook(client: OmemClient, containerTags: string[], tui:
465
496
  }
466
497
  }
467
498
  }
468
-
469
- try {
470
- const results = await client.searchMemories("*", 20, undefined, containerTags);
471
- const block = buildContextBlock(results);
472
- if (block) {
473
- output.context.push(block);
474
- }
475
- } catch {
476
- }
477
499
  };
478
500
  }
479
501
 
@@ -481,7 +503,7 @@ const processedMessageIds = new Set<string>();
481
503
  const pluginStartTime = Date.now();
482
504
 
483
505
  export function sessionIdleHook(
484
- omemClient: OmemClient,
506
+ cerebroClient: CerebroClient,
485
507
  _containerTags: string[],
486
508
  tui: any,
487
509
  sdkClient: any,
@@ -490,6 +512,7 @@ export function sessionIdleHook(
490
512
  getMainSessionId?: () => string | undefined,
491
513
  isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
492
514
  agentId?: string,
515
+ config: Partial<OmemPluginConfig> = {},
493
516
  ) {
494
517
  let idleTimeout: ReturnType<typeof setTimeout> | null = null;
495
518
  let isCapturing = false;
@@ -548,7 +571,13 @@ export function sessionIdleHook(
548
571
  if (!hasNewMessages || conversationMessages.length === 0) return;
549
572
 
550
573
  if (threshold > 1 && conversationMessages.length < threshold) {
551
- // Log that we're waiting for more messages
574
+ return;
575
+ }
576
+
577
+ // Policy gate: only readwrite agents can write memories
578
+ const policy = resolveAgentPolicy(agentId || "", config);
579
+ if (policy !== "readwrite") {
580
+ logInfo("sessionIdleHook blocked by policy", { agentId: agentId || "", policy });
552
581
  return;
553
582
  }
554
583
 
@@ -556,21 +585,19 @@ export function sessionIdleHook(
556
585
  let projectName: string | undefined;
557
586
  try {
558
587
  const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
559
- logDebug("sessionIdleHook sessionInfo", { sessionInfo: JSON.stringify(sessionInfo) });
560
588
  logDebug("sessionIdleHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
561
589
  sessionTitle = sessionInfo?.data?.title;
562
590
  projectName = sessionInfo?.data?.directory
563
591
  ? await detectProjectName(sessionInfo.data.directory)
564
592
  : undefined;
565
- logDebug("sessionIdleHook projectName", { projectName: String(projectName) });
566
593
  } catch (e) {
567
594
  logErr("sessionIdleHook detectProjectName failed", { error: String(e) });
568
595
  }
569
596
 
570
597
  try {
571
- logDebug("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, projectName: String(projectName), sessionId: sessionID, title: String(sessionTitle) });
572
- await omemClient.sessionIngest(conversationMessages, sessionID, agentId, sessionTitle, projectName);
573
- logDebug("sessionIdleHook sessionIngest ok");
598
+ logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, title: String(sessionTitle) });
599
+ await cerebroClient.sessionIngest(conversationMessages, sessionID, agentId, sessionTitle, projectName);
600
+ logInfo("sessionIdleHook sessionIngest ok");
574
601
  for (const id of newMessageIds) {
575
602
  processedMessageIds.add(id);
576
603
  }