@mingxy/cerebro 1.5.10 → 1.5.11
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/client.ts +12 -0
- package/src/hooks.ts +102 -52
- package/src/index.ts +2 -1
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -359,4 +359,16 @@ export class OmemClient {
|
|
|
359
359
|
);
|
|
360
360
|
return res?.recalls ?? [];
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
async sessionIngest(
|
|
364
|
+
messages: Array<{ role: string; content: string }>,
|
|
365
|
+
sessionId?: string,
|
|
366
|
+
agentId?: string,
|
|
367
|
+
): Promise<unknown> {
|
|
368
|
+
return this.post("/v1/memories/session-ingest", {
|
|
369
|
+
messages,
|
|
370
|
+
session_id: sessionId,
|
|
371
|
+
agent_id: agentId,
|
|
372
|
+
}, 60000);
|
|
373
|
+
}
|
|
362
374
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -12,31 +12,17 @@ function showToast(tui: any, title: string, message: string, variant: string = "
|
|
|
12
12
|
}, delayMs);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function extractUserRequest(content: string): string {
|
|
16
|
+
const match = content.match(/<user-request>([\s\S]*?)<\/user-request>/);
|
|
17
|
+
return match ? match[1].trim() : content;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const keywordDetectedSessions = new Set<string>();
|
|
16
21
|
const injectedMemoryIds = new Map<string, Set<string>>();
|
|
17
22
|
const firstMessages = new Map<string, string>();
|
|
18
23
|
const sessionMessages = new Map<string, Array<{ role: string; content: string }>>();
|
|
19
24
|
const profileInjectedSessions = new Set<string>();
|
|
20
25
|
|
|
21
|
-
function extractMemoryIds(result: unknown): string[] {
|
|
22
|
-
if (!result) return [];
|
|
23
|
-
if (Array.isArray(result)) {
|
|
24
|
-
return (result as Array<{ id?: string }>).map((m) => m.id).filter(Boolean) as string[];
|
|
25
|
-
}
|
|
26
|
-
if (typeof result === "object" && result !== null) {
|
|
27
|
-
const r = result as Record<string, unknown>;
|
|
28
|
-
if (Array.isArray(r.memories)) {
|
|
29
|
-
return (r.memories as Array<{ id?: string }>).map((m) => m.id).filter(Boolean) as string[];
|
|
30
|
-
}
|
|
31
|
-
if (Array.isArray(r.results)) {
|
|
32
|
-
return (r.results as Array<{ id?: string; memory?: { id?: string } }>)
|
|
33
|
-
.map((m) => m.id ?? m.memory?.id)
|
|
34
|
-
.filter(Boolean) as string[];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
function formatRelativeAge(isoDate: string): string {
|
|
41
27
|
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
42
28
|
const minutes = Math.floor(diffMs / 60_000);
|
|
@@ -149,7 +135,8 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
149
135
|
try {
|
|
150
136
|
const messages = sessionMessages.get(input.sessionID) ?? [];
|
|
151
137
|
const userMessages = messages.filter((m) => m.role === "user");
|
|
152
|
-
const
|
|
138
|
+
const rawQuery = userMessages[userMessages.length - 1]?.content || firstMessages.get(input.sessionID) || "";
|
|
139
|
+
const query_text = extractUserRequest(rawQuery);
|
|
153
140
|
const last_query_text = userMessages.length >= 2 ? userMessages[userMessages.length - 2].content : undefined;
|
|
154
141
|
|
|
155
142
|
const projectTags = containerTags.filter(t => t.startsWith("omem_project_"));
|
|
@@ -278,7 +265,7 @@ export function autoRecallHook(client: OmemClient, containerTags: string[], tui:
|
|
|
278
265
|
};
|
|
279
266
|
}
|
|
280
267
|
|
|
281
|
-
export function keywordDetectionHook(
|
|
268
|
+
export function keywordDetectionHook(_client: OmemClient, _containerTags: string[], threshold: number, _tui: any, _ingestMode: "smart" | "raw" = "smart") {
|
|
282
269
|
return async (
|
|
283
270
|
input: { sessionID: string; messageID?: string },
|
|
284
271
|
output: { message: UserMessage; parts: Part[] },
|
|
@@ -307,38 +294,10 @@ export function keywordDetectionHook(client: OmemClient, containerTags: string[]
|
|
|
307
294
|
});
|
|
308
295
|
|
|
309
296
|
const messages = sessionMessages.get(input.sessionID)!;
|
|
297
|
+
// Ingest is now handled by sessionIdleHook (session.idle → sessionIngest API).
|
|
298
|
+
// This hook only collects messages and detects keywords for recall.
|
|
310
299
|
if (messages.length >= threshold) {
|
|
311
|
-
|
|
312
|
-
const result = await client.ingestMessages(messages, {
|
|
313
|
-
mode: ingestMode,
|
|
314
|
-
tags: [...containerTags, "auto-capture"],
|
|
315
|
-
sessionId: input.sessionID,
|
|
316
|
-
});
|
|
317
|
-
if (result === null) {
|
|
318
|
-
showToast(tui, "🔴 Capture Failed", `Memory capture blocked · check API Key and spiritual connection`, "error");
|
|
319
|
-
} else {
|
|
320
|
-
showToast(tui, "🧠 Memory Sealed", `${messages.length} dialogues captured · entrusted to the heavens for refinement`, "success");
|
|
321
|
-
const memoryIds = extractMemoryIds(result);
|
|
322
|
-
if (memoryIds.length > 0) {
|
|
323
|
-
const recordResult = await client.recordSessionRecall(
|
|
324
|
-
input.sessionID,
|
|
325
|
-
memoryIds,
|
|
326
|
-
"auto",
|
|
327
|
-
firstMessages.get(input.sessionID) || "",
|
|
328
|
-
0,
|
|
329
|
-
0,
|
|
330
|
-
);
|
|
331
|
-
if (recordResult) {
|
|
332
|
-
showToast(tui, "📦 Capture Recorded", `${memoryIds.length} memory(s) saved to session history`, "success");
|
|
333
|
-
} else {
|
|
334
|
-
showToast(tui, "🔴 Capture Record Failed", `Failed to save capture record · check API connection`, "error");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
sessionMessages.delete(input.sessionID);
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
showToast(tui, "🔴 Capture Failed", "Memory capture blocked · spiritual pulse anomaly", "error");
|
|
341
|
-
}
|
|
300
|
+
// Threshold reached — messages will be processed on next session.idle
|
|
342
301
|
}
|
|
343
302
|
};
|
|
344
303
|
}
|
|
@@ -379,3 +338,94 @@ export function compactingHook(client: OmemClient, containerTags: string[], tui:
|
|
|
379
338
|
}
|
|
380
339
|
};
|
|
381
340
|
}
|
|
341
|
+
|
|
342
|
+
const processedMessageIds = new Set<string>();
|
|
343
|
+
const pluginStartTime = Date.now();
|
|
344
|
+
|
|
345
|
+
export function sessionIdleHook(
|
|
346
|
+
omemClient: OmemClient,
|
|
347
|
+
_containerTags: string[],
|
|
348
|
+
tui: any,
|
|
349
|
+
sdkClient: any,
|
|
350
|
+
_ingestMode: "smart" | "raw" = "smart",
|
|
351
|
+
threshold: number = 0,
|
|
352
|
+
getMainSessionId?: () => string | undefined,
|
|
353
|
+
) {
|
|
354
|
+
let idleTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
355
|
+
let isCapturing = false;
|
|
356
|
+
|
|
357
|
+
return async (input: { event: { type: string; properties?: any } }) => {
|
|
358
|
+
if (input.event.type !== "session.idle") return;
|
|
359
|
+
|
|
360
|
+
const sessionID = input.event.properties?.sessionID;
|
|
361
|
+
if (!sessionID) return;
|
|
362
|
+
|
|
363
|
+
if (getMainSessionId) {
|
|
364
|
+
const mainId = getMainSessionId();
|
|
365
|
+
if (mainId && sessionID !== mainId) return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (idleTimeout) clearTimeout(idleTimeout);
|
|
369
|
+
|
|
370
|
+
idleTimeout = setTimeout(async () => {
|
|
371
|
+
if (isCapturing) return;
|
|
372
|
+
isCapturing = true;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const response = await sdkClient.session.messages({ path: { id: sessionID } });
|
|
376
|
+
if (!response?.data) return;
|
|
377
|
+
|
|
378
|
+
const messages = response.data;
|
|
379
|
+
const conversationMessages: Array<{ role: string; content: string }> = [];
|
|
380
|
+
const newMessageIds: string[] = [];
|
|
381
|
+
let hasNewMessages = false;
|
|
382
|
+
|
|
383
|
+
for (const msg of messages) {
|
|
384
|
+
const msgId = msg.info?.id;
|
|
385
|
+
if (!msgId || processedMessageIds.has(msgId)) continue;
|
|
386
|
+
|
|
387
|
+
// Skip messages created before this plugin instance started
|
|
388
|
+
// (prevents replaying entire session history on restart)
|
|
389
|
+
const msgTime = msg.info?.createdAt ? new Date(msg.info.createdAt).getTime() : 0;
|
|
390
|
+
if (msgTime > 0 && msgTime < pluginStartTime) continue;
|
|
391
|
+
|
|
392
|
+
const role = msg.info?.role;
|
|
393
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
394
|
+
|
|
395
|
+
const textParts = (msg.parts || [])
|
|
396
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
397
|
+
.map((p: any) => p.text);
|
|
398
|
+
const text = textParts.join("\n").trim();
|
|
399
|
+
if (!text) continue;
|
|
400
|
+
|
|
401
|
+
hasNewMessages = true;
|
|
402
|
+
newMessageIds.push(msgId);
|
|
403
|
+
conversationMessages.push({ role, content: text });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!hasNewMessages || conversationMessages.length === 0) return;
|
|
407
|
+
|
|
408
|
+
if (threshold > 1 && conversationMessages.length < threshold) {
|
|
409
|
+
// Log that we're waiting for more messages
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await omemClient.sessionIngest(conversationMessages, sessionID);
|
|
415
|
+
for (const id of newMessageIds) {
|
|
416
|
+
processedMessageIds.add(id);
|
|
417
|
+
}
|
|
418
|
+
showToast(tui, "🧠 Memory Sealed", `${conversationMessages.length} dialogues captured · entrusted to the heavens for refinement`, "success");
|
|
419
|
+
} catch (err) {
|
|
420
|
+
showToast(tui, "🔴 Session Capture Failed", String(err).substring(0, 100), "error");
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
424
|
+
showToast(tui, "🔴 Idle Capture Error", errMsg.substring(0, 100), "error");
|
|
425
|
+
} finally {
|
|
426
|
+
isCapturing = false;
|
|
427
|
+
idleTimeout = null;
|
|
428
|
+
}
|
|
429
|
+
}, 10000);
|
|
430
|
+
};
|
|
431
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { OmemClient } from "./client.js";
|
|
6
|
-
import { autoRecallHook, compactingHook, keywordDetectionHook } from "./hooks.js";
|
|
6
|
+
import { autoRecallHook, compactingHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
|
|
7
7
|
import { getUserTag, getProjectTag } from "./tags.js";
|
|
8
8
|
import { buildTools } from "./tools.js";
|
|
9
9
|
import { logInfo, logError } from "./logger.js";
|
|
@@ -96,6 +96,7 @@ const OmemPlugin: Plugin = async (input) => {
|
|
|
96
96
|
"chat.message": keywordDetectionHook(omemClient, containerTags, config.autoCaptureThreshold, tui, config.ingestMode),
|
|
97
97
|
"experimental.session.compacting": compactingHook(omemClient, containerTags, tui, config.ingestMode),
|
|
98
98
|
tool: buildTools(omemClient, containerTags, { agentId, getSessionId: () => currentSessionId }),
|
|
99
|
+
event: sessionIdleHook(omemClient, containerTags, tui, client, config.ingestMode, config.autoCaptureThreshold, () => currentSessionId),
|
|
99
100
|
"shell.env": async (_input: any, output: any) => {
|
|
100
101
|
if (directory) {
|
|
101
102
|
output.env.OMEM_PROJECT_DIR = directory;
|