@mingxy/cerebro 1.10.9 → 1.10.10
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 +1 -1
- package/src/config.ts +36 -4
- package/src/hooks.ts +33 -14
- package/src/index.ts +13 -6
- package/src/tools.ts +5 -1
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -144,6 +144,25 @@ function deepMerge(base: OmemPluginConfig, overrides: Partial<OmemPluginConfig>)
|
|
|
144
144
|
|
|
145
145
|
// ── Load config ──────────────────────────────────────────────────────
|
|
146
146
|
|
|
147
|
+
/** File-only logger for config.ts (cannot import logger.ts due to circular dependency). */
|
|
148
|
+
function configLog(message: string, fields?: Record<string, unknown>): void {
|
|
149
|
+
try {
|
|
150
|
+
const logDir = join(homedir(), ".config", "cerebro", "logs");
|
|
151
|
+
const logPath = join(logDir, "plugin.log");
|
|
152
|
+
const ts = new Date().toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
153
|
+
const parts = [`WARN ${ts} service=cerebro ${message}`];
|
|
154
|
+
if (fields) {
|
|
155
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
156
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
mkdirSync(logDir, { recursive: true });
|
|
160
|
+
appendFileSync(logPath, parts.join(" ") + "\n");
|
|
161
|
+
} catch (writeErr) {
|
|
162
|
+
process.stderr.write(`[cerebro] configLog write failed: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}\n`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
147
166
|
export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPluginConfig {
|
|
148
167
|
let config: OmemPluginConfig = structuredClone(DEFAULTS);
|
|
149
168
|
|
|
@@ -157,8 +176,8 @@ export function loadPluginConfig(overrides?: Partial<OmemPluginConfig>): OmemPlu
|
|
|
157
176
|
|
|
158
177
|
// Merge nested groups with defaults for safety
|
|
159
178
|
config = deepMerge(config, parsed);
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
179
|
+
} catch (e) {
|
|
180
|
+
configLog("config.json load failed, using defaults", { error: String(e) });
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
// Apply environment variable overrides (flat OMEM_* → nested paths)
|
|
@@ -201,7 +220,20 @@ export function resolveAgentPolicy(
|
|
|
201
220
|
agentName: string,
|
|
202
221
|
config: Partial<OmemPluginConfig>,
|
|
203
222
|
): AgentPolicy {
|
|
204
|
-
|
|
223
|
+
const policies = config.agentMemoryPolicy;
|
|
224
|
+
if (policies) {
|
|
225
|
+
const exact = policies[agentName];
|
|
226
|
+
if (exact) return exact;
|
|
227
|
+
const lower = agentName.toLowerCase();
|
|
228
|
+
for (const [key, policy] of Object.entries(policies)) {
|
|
229
|
+
if (lower.startsWith(key.toLowerCase()) || key.toLowerCase().startsWith(lower)) {
|
|
230
|
+
return policy;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (config.defaultPolicy) return config.defaultPolicy;
|
|
235
|
+
configLog("resolveAgentPolicy: no policy configured, defaulting to readwrite", { agentName });
|
|
236
|
+
return "readwrite";
|
|
205
237
|
}
|
|
206
238
|
|
|
207
239
|
export { DEFAULTS };
|
package/src/hooks.ts
CHANGED
|
@@ -482,6 +482,15 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
482
482
|
} catch {
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
// Main session gate: sub-agents must not write memories via compacting
|
|
486
|
+
if (getMainSessionId) {
|
|
487
|
+
const mainId = getMainSessionId();
|
|
488
|
+
if (mainId && input.sessionID && input.sessionID !== mainId) {
|
|
489
|
+
logInfo("compactingHook: non-main session skipped", { sessionID: input.sessionID, mainSessionId: mainId });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
485
494
|
// Policy gate: only readwrite agents can write memories
|
|
486
495
|
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
487
496
|
if (policy !== "readwrite") {
|
|
@@ -515,12 +524,13 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
|
|
|
515
524
|
}
|
|
516
525
|
|
|
517
526
|
try {
|
|
518
|
-
logInfo("compactingHook ingestMessages called", { msgCount: messages.length, sessionId: effectiveSessionId });
|
|
527
|
+
logInfo("compactingHook ingestMessages called", { msgCount: messages.length, sessionId: effectiveSessionId, agentId: effectiveAgentId });
|
|
519
528
|
const result = await client.ingestMessages(messages, {
|
|
520
529
|
mode: ingestMode,
|
|
521
530
|
tags: [...containerTags, "auto-capture"],
|
|
522
531
|
sessionId: effectiveSessionId,
|
|
523
532
|
projectName: projectName,
|
|
533
|
+
agentId: effectiveAgentId,
|
|
524
534
|
});
|
|
525
535
|
logInfo("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
|
|
526
536
|
if (result === null) {
|
|
@@ -553,6 +563,7 @@ export function sessionIdleHook(
|
|
|
553
563
|
isAutoStoreEnabled?: (sessionId: string | undefined) => boolean,
|
|
554
564
|
agentId?: string,
|
|
555
565
|
config: Partial<OmemPluginConfig> = {},
|
|
566
|
+
onAgentResolved?: (name: string) => void,
|
|
556
567
|
) {
|
|
557
568
|
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
558
569
|
let isCapturing = false;
|
|
@@ -560,6 +571,8 @@ export function sessionIdleHook(
|
|
|
560
571
|
return async (input: { event: { type: string; properties?: any } }) => {
|
|
561
572
|
if (input.event.type !== "session.idle") return;
|
|
562
573
|
|
|
574
|
+
logDebug("sessionIdleHook event.properties dump", { keys: Object.keys(input.event.properties || {}), raw: JSON.stringify(input.event.properties).substring(0, 2000) });
|
|
575
|
+
|
|
563
576
|
const sessionID = input.event.properties?.sessionID;
|
|
564
577
|
if (!sessionID) return;
|
|
565
578
|
|
|
@@ -567,7 +580,10 @@ export function sessionIdleHook(
|
|
|
567
580
|
|
|
568
581
|
if (getMainSessionId) {
|
|
569
582
|
const mainId = getMainSessionId();
|
|
570
|
-
if (mainId && sessionID !== mainId)
|
|
583
|
+
if (mainId && sessionID !== mainId) {
|
|
584
|
+
logInfo("sessionIdleHook: non-main session skipped", { sessionID, mainSessionId: mainId });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
571
587
|
}
|
|
572
588
|
|
|
573
589
|
if (idleTimeout) clearTimeout(idleTimeout);
|
|
@@ -589,8 +605,6 @@ export function sessionIdleHook(
|
|
|
589
605
|
const msgId = msg.info?.id;
|
|
590
606
|
if (!msgId || processedMessageIds.has(msgId)) continue;
|
|
591
607
|
|
|
592
|
-
// Skip messages created before this plugin instance started
|
|
593
|
-
// (prevents replaying entire session history on restart)
|
|
594
608
|
const msgTime = msg.info?.createdAt ? new Date(msg.info.createdAt).getTime() : 0;
|
|
595
609
|
if (msgTime > 0 && msgTime < pluginStartTime) continue;
|
|
596
610
|
|
|
@@ -614,18 +628,15 @@ export function sessionIdleHook(
|
|
|
614
628
|
return;
|
|
615
629
|
}
|
|
616
630
|
|
|
617
|
-
// Policy gate: only readwrite agents can write memories
|
|
618
|
-
const policy = resolveAgentPolicy(agentId || "", config);
|
|
619
|
-
if (policy !== "readwrite") {
|
|
620
|
-
logInfo("sessionIdleHook blocked by policy", { agentId: agentId || "", policy });
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
631
|
let sessionTitle: string | undefined;
|
|
625
632
|
let projectName: string | undefined;
|
|
633
|
+
let effectiveAgentId = agentId || "opencode";
|
|
626
634
|
try {
|
|
627
635
|
const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
|
|
628
|
-
|
|
636
|
+
if ((sessionInfo?.data as any)?.agent) {
|
|
637
|
+
effectiveAgentId = (sessionInfo.data as any).agent;
|
|
638
|
+
onAgentResolved?.(effectiveAgentId);
|
|
639
|
+
}
|
|
629
640
|
sessionTitle = sessionInfo?.data?.title;
|
|
630
641
|
projectName = sessionInfo?.data?.directory
|
|
631
642
|
? await detectProjectName(sessionInfo.data.directory)
|
|
@@ -634,9 +645,17 @@ export function sessionIdleHook(
|
|
|
634
645
|
logErr("sessionIdleHook detectProjectName failed", { error: String(e) });
|
|
635
646
|
}
|
|
636
647
|
|
|
648
|
+
logDebug("sessionIdleHook resolved agentId", { effectiveAgentId, fallbackAgentId: agentId });
|
|
649
|
+
|
|
650
|
+
const policy = resolveAgentPolicy(effectiveAgentId, config);
|
|
651
|
+
if (policy !== "readwrite") {
|
|
652
|
+
logInfo("sessionIdleHook blocked by policy", { agentId: effectiveAgentId, policy, defaultPolicy: String(config.defaultPolicy ?? "undefined") });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
637
656
|
try {
|
|
638
|
-
logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, title: String(sessionTitle) });
|
|
639
|
-
await cerebroClient.sessionIngest(conversationMessages, sessionID,
|
|
657
|
+
logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, agentId: effectiveAgentId, title: String(sessionTitle) });
|
|
658
|
+
await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName);
|
|
640
659
|
logInfo("sessionIdleHook sessionIngest ok");
|
|
641
660
|
for (const id of newMessageIds) {
|
|
642
661
|
processedMessageIds.add(id);
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { CerebroClient } from "./client.js";
|
|
|
7
7
|
import { autoRecallHook, compactingHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
|
|
8
8
|
import { getUserTag, getProjectTag } from "./tags.js";
|
|
9
9
|
import { buildTools } from "./tools.js";
|
|
10
|
-
import { logInfo, logError } from "./logger.js";
|
|
10
|
+
import { logInfo, logDebug, logError } from "./logger.js";
|
|
11
11
|
import { loadPluginConfig } from "./config.js";
|
|
12
12
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -115,7 +115,9 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
115
115
|
const containerTags = [getUserTag(email), getProjectTag(cwd)];
|
|
116
116
|
const agentId = process.env.OMEM_AGENT_ID || "opencode";
|
|
117
117
|
|
|
118
|
-
let
|
|
118
|
+
let mainSessionId: string | undefined;
|
|
119
|
+
let mainSessionLocked = false;
|
|
120
|
+
let cachedAgentName: string | undefined;
|
|
119
121
|
|
|
120
122
|
const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config);
|
|
121
123
|
|
|
@@ -128,13 +130,18 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
128
130
|
};
|
|
129
131
|
},
|
|
130
132
|
"experimental.chat.system.transform": async (input: any, output: any) => {
|
|
131
|
-
|
|
133
|
+
logDebug("transform input", { sessionID: input.sessionID });
|
|
134
|
+
if (input.sessionID && !mainSessionLocked) {
|
|
135
|
+
mainSessionId = input.sessionID;
|
|
136
|
+
mainSessionLocked = true;
|
|
137
|
+
logInfo("mainSessionId locked", { sessionId: input.sessionID });
|
|
138
|
+
}
|
|
132
139
|
return recallHook(input, output);
|
|
133
140
|
},
|
|
134
141
|
"chat.message": keywordDetectionHook(cerebroClient, containerTags, config.ingest.autoCaptureThreshold, tui, config.ingest.ingestMode, config, agentId),
|
|
135
|
-
"experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () =>
|
|
136
|
-
tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () =>
|
|
137
|
-
event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () =>
|
|
142
|
+
"experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId),
|
|
143
|
+
tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId }),
|
|
144
|
+
event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }),
|
|
138
145
|
"shell.env": async (_input: any, output: any) => {
|
|
139
146
|
if (directory) {
|
|
140
147
|
output.env.OMEM_PROJECT_DIR = directory;
|
package/src/tools.ts
CHANGED
|
@@ -24,6 +24,7 @@ function extractMemoryIds(result: unknown): string[] {
|
|
|
24
24
|
export interface ToolContext {
|
|
25
25
|
agentId?: string;
|
|
26
26
|
getSessionId: () => string | undefined;
|
|
27
|
+
getAgentName?: () => string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function buildTools(client: CerebroClient, containerTags: string[], context: ToolContext) {
|
|
@@ -85,12 +86,13 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
|
|
|
85
86
|
},
|
|
86
87
|
async execute(args) {
|
|
87
88
|
const allTags = [...containerTags, ...(args.tags ?? [])];
|
|
89
|
+
const effectiveAgentId = context.getAgentName?.() || context.agentId;
|
|
88
90
|
const result = await client.createMemory(
|
|
89
91
|
args.content,
|
|
90
92
|
allTags,
|
|
91
93
|
args.source,
|
|
92
94
|
args.scope ?? "project",
|
|
93
|
-
|
|
95
|
+
effectiveAgentId,
|
|
94
96
|
context.getSessionId(),
|
|
95
97
|
args.visibility,
|
|
96
98
|
args.category,
|
|
@@ -241,10 +243,12 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
|
|
|
241
243
|
.describe("Session ID to associate with the ingestion"),
|
|
242
244
|
},
|
|
243
245
|
async execute(args) {
|
|
246
|
+
const effectiveAgentId = context.getAgentName?.() || context.agentId;
|
|
244
247
|
const result = await client.ingestMessages(args.messages, {
|
|
245
248
|
mode: args.mode ?? "smart",
|
|
246
249
|
tags: args.tags,
|
|
247
250
|
sessionId: args.session_id,
|
|
251
|
+
agentId: effectiveAgentId,
|
|
248
252
|
});
|
|
249
253
|
if (result === null) return JSON.stringify({ ok: false, error: "Ingestion failed" });
|
|
250
254
|
if (args.session_id) {
|