@mingxy/cerebro 1.11.11 → 1.11.12

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/hooks.ts +107 -103
  3. package/src/logger.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/cerebro",
3
- "version": "1.11.11",
3
+ "version": "1.11.12",
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/src/hooks.ts CHANGED
@@ -567,6 +567,8 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
567
567
  input: { sessionID?: string },
568
568
  output: { context: string[]; prompt?: string },
569
569
  ) => {
570
+ logInfo("compactingHook triggered", { sessionId: input.sessionID, hasSessionMessages: sessionMessages.has(input.sessionID || "") });
571
+
570
572
  // Search (read) always runs — even readonly agents need context during compacting
571
573
  try {
572
574
  const results = await client.searchMemories("*", 20, undefined, containerTags);
@@ -600,6 +602,23 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
600
602
  return;
601
603
  }
602
604
 
605
+ const effectiveSessionId = (getMainSessionId?.() || input.sessionID);
606
+
607
+ // Resolve project name (shared by ingest + poll)
608
+ let projectName: string | undefined;
609
+ try {
610
+ if (sdkClient && input.sessionID) {
611
+ const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
612
+ logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
613
+ projectName = sessionInfo?.data?.directory
614
+ ? await detectProjectName(sessionInfo.data.directory)
615
+ : undefined;
616
+ }
617
+ } catch (e) {
618
+ logErr("compactingHook detectProjectName failed", { error: String(e) });
619
+ }
620
+
621
+ // --- Phase 1: Ingest tracked messages from sessionMessages (if available) ---
603
622
  if (input.sessionID && sessionMessages.has(input.sessionID)) {
604
623
  if (isAutoStoreEnabled && !isAutoStoreEnabled(input.sessionID)) {
605
624
  sessionMessages.delete(input.sessionID);
@@ -609,21 +628,6 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
609
628
  } else {
610
629
  const messages = sessionMessages.get(input.sessionID)!;
611
630
  if (messages.length > 0) {
612
- const effectiveSessionId = (getMainSessionId?.() || input.sessionID);
613
-
614
- let projectName: string | undefined;
615
- try {
616
- if (sdkClient && input.sessionID) {
617
- const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
618
- logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
619
- projectName = sessionInfo?.data?.directory
620
- ? await detectProjectName(sessionInfo.data.directory)
621
- : undefined;
622
- }
623
- } catch (e) {
624
- logErr("compactingHook detectProjectName failed", { error: String(e) });
625
- }
626
-
627
631
  try {
628
632
  logInfo("compactingHook ingestMessages called", { msgCount: messages.length, sessionId: effectiveSessionId, agentId: effectiveAgentId });
629
633
  const result = await client.ingestMessages(messages, {
@@ -643,105 +647,105 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
643
647
  logErr("compactingHook ingestMessages failed", { error: String(e) });
644
648
  showToast(tui, "🔴 Archive Failed", "Session archive blocked · spiritual pulse anomaly", "error");
645
649
  }
650
+ }
651
+ }
652
+ // Cleanup tracked messages regardless of ingest result
653
+ sessionMessages.delete(input.sessionID);
654
+ profileInjectedSessions.delete(input.sessionID);
655
+ injectedMemoryIds.delete(input.sessionID);
656
+ firstMessages.delete(input.sessionID);
657
+ }
646
658
 
647
- // === Async poll: wait for compact to complete, then extract summary ===
648
- if (sdkClient && input.sessionID) {
649
- const pollSessionId = input.sessionID;
650
- const pollEffectiveSessionId = effectiveSessionId;
651
- const pollProjectName = projectName;
652
- const pollAgentId = effectiveAgentId;
659
+ // --- Phase 2: Async poll for compact summary (independent of sessionMessages) ---
660
+ if (sdkClient && input.sessionID) {
661
+ const pollSessionId = input.sessionID;
662
+ const pollEffectiveSessionId = effectiveSessionId;
663
+ const pollProjectName = projectName;
664
+ const pollAgentId = effectiveAgentId;
653
665
 
654
- let baselineMsgCount: number | undefined;
655
- try {
656
- const preResp = await sdkClient.session.messages({ path: { id: pollSessionId } });
657
- baselineMsgCount = preResp?.data?.length;
658
- logInfo("compactingHook: summary poll starting", { baselineMsgCount, sessionId: pollSessionId });
659
- } catch (e) {
660
- logErr("compactingHook: baseline msg count failed", { error: String(e) });
661
- }
666
+ let baselineMsgCount: number | undefined;
667
+ try {
668
+ const preResp = await sdkClient.session.messages({ path: { id: pollSessionId } });
669
+ baselineMsgCount = preResp?.data?.length;
670
+ logInfo("compactingHook: summary poll starting", { baselineMsgCount, sessionId: pollSessionId });
671
+ } catch (e) {
672
+ logErr("compactingHook: baseline msg count failed", { error: String(e) });
673
+ }
662
674
 
663
- if (baselineMsgCount !== undefined && baselineMsgCount > 0) {
664
- const maxAttempts = 12;
665
- const pollInterval = 5000;
666
-
667
- // Fire-and-forget — MUST NOT await (would block the compacting hook)
668
- (async () => {
669
- for (let i = 0; i < maxAttempts; i++) {
670
- await new Promise(r => setTimeout(r, pollInterval));
671
- try {
672
- const resp = await sdkClient.session.messages({ path: { id: pollSessionId } });
673
- if (!resp?.data) continue;
674
- const currentCount = resp.data.length;
675
-
676
- logDebug("compactingHook: summary poll tick", {
677
- attempt: i + 1, currentCount, baselineMsgCount,
678
- });
675
+ if (baselineMsgCount !== undefined && baselineMsgCount > 0) {
676
+ const maxAttempts = 12;
677
+ const pollInterval = 5000;
679
678
 
680
- if (currentCount < baselineMsgCount) {
681
- logInfo("compactingHook: compact completed detected", {
682
- preCount: baselineMsgCount, postCount: currentCount, attempt: i + 1,
679
+ // Fire-and-forget — MUST NOT await (would block the compacting hook)
680
+ (async () => {
681
+ for (let i = 0; i < maxAttempts; i++) {
682
+ await new Promise(r => setTimeout(r, pollInterval));
683
+ try {
684
+ const resp = await sdkClient.session.messages({ path: { id: pollSessionId } });
685
+ if (!resp?.data) continue;
686
+ const currentCount = resp.data.length;
687
+
688
+ logDebug("compactingHook: summary poll tick", {
689
+ attempt: i + 1, currentCount, baselineMsgCount,
690
+ });
691
+
692
+ if (currentCount < baselineMsgCount) {
693
+ logInfo("compactingHook: compact completed detected", {
694
+ preCount: baselineMsgCount, postCount: currentCount, attempt: i + 1,
695
+ });
696
+
697
+ const msgDump = resp.data.map((m: any) => ({
698
+ role: m.info?.role,
699
+ id: m.info?.id,
700
+ partTypes: (m.parts || []).map((p: any) => p.type),
701
+ textPreview: (m.parts || [])
702
+ .filter((p: any) => p.type === "text" && p.text)
703
+ .map((p: any) => p.text.substring(0, 80))
704
+ .join(" | "),
705
+ }));
706
+ logDebug("compactingHook: post-compact message dump", { messages: msgDump });
707
+
708
+ const userMsgs = resp.data.filter((m: any) => m.info?.role === "user");
709
+ for (const um of userMsgs) {
710
+ const textParts = (um.parts || [])
711
+ .filter((p: any) => p.type === "text" && p.text)
712
+ .map((p: any) => p.text);
713
+ const summaryText = textParts.join("\n").trim();
714
+
715
+ if (summaryText && summaryText.length > 100) {
716
+ logInfo("compactingHook: storing compact summary", {
717
+ summaryLen: summaryText.length, msgId: um.info?.id,
718
+ });
719
+ try {
720
+ const result = await client.ingestMessages(
721
+ [{ role: "user" as const, content: summaryText }],
722
+ {
723
+ mode: ingestMode,
724
+ tags: [...containerTags, "auto-capture", "compact-summary"],
725
+ sessionId: pollEffectiveSessionId,
726
+ projectName: pollProjectName,
727
+ agentId: pollAgentId,
728
+ },
729
+ );
730
+ logInfo("compactingHook: compact summary store result", {
731
+ result: result === null ? "null(blocked)" : "ok",
683
732
  });
684
-
685
- const msgDump = resp.data.map((m: any) => ({
686
- role: m.info?.role,
687
- id: m.info?.id,
688
- partTypes: (m.parts || []).map((p: any) => p.type),
689
- textPreview: (m.parts || [])
690
- .filter((p: any) => p.type === "text" && p.text)
691
- .map((p: any) => p.text.substring(0, 80))
692
- .join(" | "),
693
- }));
694
- logDebug("compactingHook: post-compact message dump", { messages: msgDump });
695
-
696
- const userMsgs = resp.data.filter((m: any) => m.info?.role === "user");
697
- for (const um of userMsgs) {
698
- const textParts = (um.parts || [])
699
- .filter((p: any) => p.type === "text" && p.text)
700
- .map((p: any) => p.text);
701
- const summaryText = textParts.join("\n").trim();
702
-
703
- if (summaryText && summaryText.length > 100) {
704
- logInfo("compactingHook: storing compact summary", {
705
- summaryLen: summaryText.length, msgId: um.info?.id,
706
- });
707
- try {
708
- const result = await client.ingestMessages(
709
- [{ role: "user" as const, content: summaryText }],
710
- {
711
- mode: ingestMode,
712
- tags: [...containerTags, "auto-capture", "compact-summary"],
713
- sessionId: pollEffectiveSessionId,
714
- projectName: pollProjectName,
715
- agentId: pollAgentId,
716
- },
717
- );
718
- logInfo("compactingHook: compact summary store result", {
719
- result: result === null ? "null(blocked)" : "ok",
720
- });
721
- if (result !== null) {
722
- showToast(tui, "📦 Compact Summary Stored", "Session summary archived to memory", "success");
723
- }
724
- } catch (e) {
725
- logErr("compactingHook: compact summary store failed", { error: String(e) });
726
- }
727
- break;
728
- }
733
+ if (result !== null) {
734
+ showToast(tui, "📦 Compact Summary Stored", "Session summary archived to memory", "success");
729
735
  }
730
- break;
736
+ } catch (e) {
737
+ logErr("compactingHook: compact summary store failed", { error: String(e) });
731
738
  }
732
- } catch (e) {
733
- logErr("compactingHook: summary poll error", { error: String(e), attempt: i + 1 });
739
+ break;
734
740
  }
735
741
  }
736
- })();
742
+ break;
743
+ }
744
+ } catch (e) {
745
+ logErr("compactingHook: summary poll error", { error: String(e), attempt: i + 1 });
737
746
  }
738
747
  }
739
-
740
- sessionMessages.delete(input.sessionID);
741
- profileInjectedSessions.delete(input.sessionID);
742
- injectedMemoryIds.delete(input.sessionID);
743
- firstMessages.delete(input.sessionID);
744
- }
748
+ })();
745
749
  }
746
750
  }
747
751
  };
package/src/logger.ts CHANGED
@@ -34,7 +34,8 @@ function writeLog(level: string, message: string, fields?: Record<string, unknow
34
34
  const nowMs = now.getTime();
35
35
  const delta = ((nowMs - lastLogTime) / 1000).toFixed(2);
36
36
  lastLogTime = nowMs;
37
- const ts = now.toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
37
+ const pad = (n: number) => String(n).padStart(2, "0");
38
+ const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
38
39
  const parts = [`${level.padEnd(5)} ${ts} +${delta}s service=cerebro`];
39
40
  if (fields) {
40
41
  for (const [k, v] of Object.entries(fields)) {