@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/cerebro.example.jsonc +71 -0
- package/dist/client.d.ts +2 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -15
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +30 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +101 -46
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts +6 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +86 -47
- package/dist/hooks.js.map +1 -1
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +4 -4
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +42 -23
- package/dist/logger.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +47 -47
- package/src/client.ts +13 -14
- package/src/config.ts +207 -104
- package/src/hooks.ts +69 -42
- package/src/index.ts +13 -13
- package/src/logger.ts +65 -65
- package/src/tools.ts +3 -3
- package/omem.example.jsonc +0 -22
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 {
|
|
3
|
-
import type
|
|
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
|
-
"<
|
|
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
|
-
"</
|
|
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
|
-
"<
|
|
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
|
-
"</
|
|
228
|
+
"</cerebro-context>",
|
|
229
229
|
].join("\n");
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export function autoRecallHook(client:
|
|
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
|
-
"<
|
|
271
|
+
"<cerebro-profile>",
|
|
265
272
|
JSON.stringify(profile, null, 2),
|
|
266
|
-
"</
|
|
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("[
|
|
368
|
+
if (errMsg.includes("[cerebro]")) {
|
|
359
369
|
// Server returned error (500, etc.) with details
|
|
360
|
-
const cleanMsg = errMsg.replace(/^\[
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
await
|
|
573
|
-
|
|
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
|
}
|