@mingxy/cerebro 1.12.8 → 1.13.0

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/hooks.ts CHANGED
@@ -297,7 +297,7 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
297
297
  ].join("\n");
298
298
  }
299
299
 
300
- export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}, getAgentName?: () => string) {
300
+ export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}, getAgentName?: () => string, directory?: string) {
301
301
  const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
302
302
  const maxRecallResults = config.recall?.maxRecallResults ?? 10;
303
303
  const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
@@ -367,6 +367,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
367
367
  refine_strategy: refineStrategy,
368
368
  refine_medium_chars: refineMediumChars,
369
369
  },
370
+ directory || process.env.OMEM_PROJECT_DIR,
370
371
  );
371
372
 
372
373
  if (!shouldRecallRes) {
@@ -626,7 +627,7 @@ export function keywordDetectionHook(_client: CerebroClient, _containerTags: str
626
627
  };
627
628
  }
628
629
 
629
- 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) {
630
+ 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, directory?: string) {
630
631
  const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
631
632
  return async (
632
633
  input: { sessionID?: string },
@@ -677,10 +678,12 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
677
678
 
678
679
  // Resolve project name (shared by ingest + poll)
679
680
  let projectName: string | undefined;
681
+ let projectPath: string | undefined;
680
682
  try {
681
683
  if (sdkClient && input.sessionID) {
682
684
  const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
683
685
  logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
686
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
684
687
  projectName = sessionInfo?.data?.directory
685
688
  ? await detectProjectName(sessionInfo.data.directory)
686
689
  : undefined;
@@ -688,6 +691,9 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
688
691
  } catch (e) {
689
692
  logErr("compactingHook detectProjectName failed", { error: String(e) });
690
693
  }
694
+ if (!projectPath) {
695
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
696
+ }
691
697
 
692
698
  // --- Phase 1: Ingest tracked messages from sessionMessages (if available) ---
693
699
  if (input.sessionID && sessionMessages.has(input.sessionID)) {
@@ -706,6 +712,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
706
712
  sessionId: effectiveSessionId,
707
713
  projectName: projectName,
708
714
  agentId: effectiveAgentId,
715
+ projectPath,
709
716
  });
710
717
  logInfo("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
711
718
  if (result === null) {
@@ -723,6 +730,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
723
730
  sessionMessages.delete(input.sessionID);
724
731
  profileInjectedSessions.delete(input.sessionID);
725
732
  firstMessages.delete(input.sessionID);
733
+ if (input.sessionID) {
734
+ const deleted = pendingToolCalls.delete(input.sessionID);
735
+ logDebug("compactingHook cleared session pendingToolCalls", { sessionID: input.sessionID, hadPending: deleted });
736
+ }
726
737
  // Evict stale injectedMemoryIds if over size cap (200 sessions)
727
738
  if (injectedMemoryIds.size > 200) {
728
739
  injectedMemoryIds.clear();
@@ -734,6 +745,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
734
745
  const pollSessionId = input.sessionID;
735
746
  const pollEffectiveSessionId = effectiveSessionId;
736
747
  const pollProjectName = projectName;
748
+ const pollProjectPath = projectPath;
737
749
  const pollAgentId = effectiveAgentId;
738
750
 
739
751
  let baselineMsgIds: Set<string> = new Set();
@@ -843,6 +855,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
843
855
  sessionId: pollEffectiveSessionId,
844
856
  projectName: pollProjectName,
845
857
  agentId: pollAgentId,
858
+ projectPath: pollProjectPath,
846
859
  },
847
860
  );
848
861
  logInfo("compactingHook: compact summary store result", {
@@ -881,6 +894,7 @@ export function autocontinueHook(
881
894
  sdkClient?: any,
882
895
  config: Partial<OmemPluginConfig> = {},
883
896
  agentId?: string,
897
+ directory?: string,
884
898
  ) {
885
899
  const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
886
900
  return async (
@@ -936,14 +950,19 @@ export function autocontinueHook(
936
950
  }
937
951
 
938
952
  let projectName: string | undefined;
953
+ let projectPath: string | undefined;
939
954
  try {
940
955
  const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
956
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
941
957
  projectName = sessionInfo?.data?.directory
942
958
  ? await detectProjectName(sessionInfo.data.directory)
943
959
  : undefined;
944
960
  } catch (e) {
945
961
  logErr("autocontinueHook detectProjectName failed", { error: String(e) });
946
962
  }
963
+ if (!projectPath) {
964
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
965
+ }
947
966
 
948
967
  const messages = [{ role: "user" as const, content: summaryText }];
949
968
  logInfo("autocontinueHook storing compact summary", {
@@ -960,6 +979,7 @@ export function autocontinueHook(
960
979
  sessionId: effectiveSessionId,
961
980
  projectName: projectName,
962
981
  agentId: effectiveAgentId,
982
+ projectPath,
963
983
  });
964
984
 
965
985
  logInfo("autocontinueHook store result", { result: result === null ? "null(blocked)" : "ok" });
@@ -977,6 +997,69 @@ export function autocontinueHook(
977
997
  const processedMessageIds = new Set<string>();
978
998
  const pluginStartTime = Date.now();
979
999
 
1000
+ // ── Soul Whisper: pending tool call tracking (per-session isolation) ──
1001
+ const pendingToolCalls = new Map<string, Map<string, { toolName: string; timestamp: number }>>();
1002
+
1003
+ export function soulWhisperToolTracker(config: OmemPluginConfig) {
1004
+ return async (input: { tool: string; sessionID: string; callID: string }, _output: { args: any }) => {
1005
+ if (config.soulWhisper?.enabled === false) {
1006
+ logDebug("soulWhisperToolTracker disabled by config", { tool: input.tool });
1007
+ return;
1008
+ }
1009
+
1010
+ const sw = config.soulWhisper;
1011
+ const toolName = input.tool;
1012
+
1013
+ const excludeTools = sw?.excludeTools ?? [];
1014
+ if (excludeTools.includes(toolName)) {
1015
+ logDebug("soulWhisperToolTracker excluded", { tool: toolName });
1016
+ return;
1017
+ }
1018
+
1019
+ const includeTools = sw?.tools ?? ["*"];
1020
+ const isWildcard = includeTools.includes("*");
1021
+ if (!isWildcard && !includeTools.includes(toolName)) {
1022
+ logDebug("soulWhisperToolTracker not in whitelist", { tool: toolName, whitelist: includeTools });
1023
+ return;
1024
+ }
1025
+
1026
+ const sid = input.sessionID || "_default";
1027
+ let sessionMap = pendingToolCalls.get(sid);
1028
+ if (!sessionMap) {
1029
+ sessionMap = new Map();
1030
+ pendingToolCalls.set(sid, sessionMap);
1031
+ }
1032
+ sessionMap.set(input.callID, { toolName, timestamp: Date.now() });
1033
+ logDebug("soulWhisperToolTracker recorded", { tool: toolName, callID: input.callID, sessionID: sid, totalSessions: pendingToolCalls.size, sessionCallCount: sessionMap.size });
1034
+ };
1035
+ }
1036
+
1037
+ function buildWhisperText(toolNames: string[], maxToolNames: number): string | null {
1038
+ if (toolNames.length === 0) return null;
1039
+
1040
+ const lines: string[] = ["<cerebro-system-reminder>"];
1041
+
1042
+ if (toolNames.length <= maxToolNames) {
1043
+ lines.push(`SOUL WHISPER — Detected tool call: ${toolNames.join(", ")}.`);
1044
+ lines.push("Your memories (cerebro-context) may contain:");
1045
+ lines.push("- Exact commands or flags someone already tested");
1046
+ lines.push("- File paths or configurations that worked before");
1047
+ lines.push("- Solutions to errors you previously encountered");
1048
+ lines.push(`If a memory matches → use it. Then reply: 💜 受到灵魂的召唤——[what you used]`);
1049
+ } else {
1050
+ lines.push("SOUL WHISPER — Memories from past sessions are loaded above (see <cerebro-context>).");
1051
+ lines.push("Before you act:");
1052
+ lines.push("1. Scan cerebro-context for commands, paths, or patterns matching your current task");
1053
+ lines.push("2. If found → use directly, do not re-derive from scratch");
1054
+ lines.push(`3. If a memory guided your action → reply: 💜 受到灵魂的召唤——[what you used]`);
1055
+ }
1056
+
1057
+ lines.push(`If memory summaries are insufficient → use memory_get("id") to fetch full content, or memory_search("query") to find more.`);
1058
+ lines.push("</cerebro-system-reminder>");
1059
+
1060
+ return lines.join("\n");
1061
+ }
1062
+
980
1063
  const FETCH_POLICY_NUDGE = [
981
1064
  "<cerebro-system-reminder>",
982
1065
  "MEMORY REMINDER: You have injected memories above (see <cerebro-context>).",
@@ -985,7 +1068,7 @@ const FETCH_POLICY_NUDGE = [
985
1068
  "</cerebro-system-reminder>",
986
1069
  ].join("\n");
987
1070
 
988
- export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
1071
+ export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean, config?: OmemPluginConfig) {
989
1072
  return async (_input: Record<string, unknown>, output: { messages: any[] }) => {
990
1073
  let shouldNudge = getContextInjectedFlag();
991
1074
  if (!shouldNudge && Array.isArray(output.messages)) {
@@ -994,12 +1077,17 @@ export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
994
1077
  m.parts.some((p: any) => typeof p.text === "string" && p.text.includes("<cerebro-context>"))
995
1078
  );
996
1079
  }
997
- if (!shouldNudge) return;
1080
+
1081
+ const swEnabled = config?.soulWhisper?.enabled !== false;
1082
+ const hasAnyPending = pendingToolCalls.size > 0;
1083
+ if (!shouldNudge && !(swEnabled && hasAnyPending)) {
1084
+ logDebug("fetchPolicyNudgeHook skipped", { shouldNudge, swEnabled, hasAnyPending });
1085
+ return;
1086
+ }
998
1087
 
999
1088
  const messages = output.messages;
1000
1089
  if (!messages || !Array.isArray(messages) || messages.length === 0) return;
1001
1090
 
1002
- // Find the last user message
1003
1091
  let lastUserIdx = -1;
1004
1092
  for (let i = messages.length - 1; i >= 0; i--) {
1005
1093
  if (messages[i]?.info?.role === "user") {
@@ -1012,13 +1100,29 @@ export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
1012
1100
  const userMsg = messages[lastUserIdx];
1013
1101
  if (!Array.isArray(userMsg.parts)) return;
1014
1102
 
1015
- // Idempotency check
1016
- const nudgeId = `cerebro_nudge_${userMsg.info.sessionID || userMsg.info.id}`;
1103
+ const sessionId = userMsg.info.sessionID || "_default";
1104
+ const nudgeId = `cerebro_nudge_${sessionId}`;
1017
1105
  for (const part of userMsg.parts) {
1018
1106
  if (part.id === nudgeId) return;
1019
1107
  }
1020
1108
 
1021
- // Find the first text part position, insert synthetic part before it
1109
+ const parts: string[] = [];
1110
+ if (shouldNudge) parts.push(FETCH_POLICY_NUDGE);
1111
+
1112
+ const sessionCalls = swEnabled ? pendingToolCalls.get(sessionId) : undefined;
1113
+ if (sessionCalls && sessionCalls.size > 0) {
1114
+ const toolNames = [...new Set([...sessionCalls.values()].map(v => v.toolName))];
1115
+ const maxToolNames = config?.soulWhisper?.maxToolNames ?? 3;
1116
+ const whisperText = buildWhisperText(toolNames, maxToolNames);
1117
+ if (whisperText) parts.push(whisperText);
1118
+ pendingToolCalls.delete(sessionId);
1119
+ logDebug("soulWhisper consumed session calls", { sessionId, callCount: sessionCalls.size, toolNames });
1120
+ } else if (swEnabled) {
1121
+ logDebug("soulWhisper no pending calls for session", { sessionId, globalSessionCount: pendingToolCalls.size });
1122
+ }
1123
+
1124
+ if (parts.length === 0) return;
1125
+
1022
1126
  const textPartIdx = userMsg.parts.findIndex((p: any) => p.type === "text" && typeof p.text === "string");
1023
1127
 
1024
1128
  const syntheticPart = {
@@ -1026,7 +1130,7 @@ export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
1026
1130
  messageID: userMsg.info.id,
1027
1131
  sessionID: userMsg.info.sessionID || "",
1028
1132
  type: "text" as const,
1029
- text: FETCH_POLICY_NUDGE,
1133
+ text: parts.join("\n\n"),
1030
1134
  synthetic: true,
1031
1135
  };
1032
1136
 
@@ -1036,7 +1140,7 @@ export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
1036
1140
  userMsg.parts.push(syntheticPart);
1037
1141
  }
1038
1142
 
1039
- logDebug("fetchPolicyNudgeHook injected", { sessionId: userMsg.info.sessionID || "", nudgeId });
1143
+ logDebug("fetchPolicyNudgeHook injected", { sessionId, nudgeId, hasWhisper: sessionCalls != null && sessionCalls.size > 0, partsCount: parts.length });
1040
1144
  };
1041
1145
  }
1042
1146
 
@@ -1052,6 +1156,7 @@ export function sessionIdleHook(
1052
1156
  agentId?: string,
1053
1157
  config: Partial<OmemPluginConfig> = {},
1054
1158
  onAgentResolved?: (name: string) => void,
1159
+ directory?: string,
1055
1160
  ) {
1056
1161
  let idleTimeout: ReturnType<typeof setTimeout> | null = null;
1057
1162
  let isCapturing = false;
@@ -1118,6 +1223,7 @@ export function sessionIdleHook(
1118
1223
 
1119
1224
  let sessionTitle: string | undefined;
1120
1225
  let projectName: string | undefined;
1226
+ let projectPath: string | undefined;
1121
1227
  let effectiveAgentId = agentId || "opencode";
1122
1228
  try {
1123
1229
  const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
@@ -1126,12 +1232,16 @@ export function sessionIdleHook(
1126
1232
  onAgentResolved?.(effectiveAgentId);
1127
1233
  }
1128
1234
  sessionTitle = sessionInfo?.data?.title;
1235
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
1129
1236
  projectName = sessionInfo?.data?.directory
1130
1237
  ? await detectProjectName(sessionInfo.data.directory)
1131
1238
  : undefined;
1132
1239
  } catch (e) {
1133
1240
  logErr("sessionIdleHook detectProjectName failed", { error: String(e) });
1134
1241
  }
1242
+ if (!projectPath) {
1243
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
1244
+ }
1135
1245
 
1136
1246
  logDebug("sessionIdleHook resolved agentId", { effectiveAgentId, fallbackAgentId: agentId });
1137
1247
 
@@ -1143,7 +1253,7 @@ export function sessionIdleHook(
1143
1253
 
1144
1254
  try {
1145
1255
  logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, agentId: effectiveAgentId, title: String(sessionTitle) });
1146
- await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName);
1256
+ await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName, projectPath);
1147
1257
  logInfo("sessionIdleHook sessionIngest ok");
1148
1258
  for (const id of newMessageIds) {
1149
1259
  processedMessageIds.add(id);
@@ -1159,6 +1269,8 @@ export function sessionIdleHook(
1159
1269
  } finally {
1160
1270
  isCapturing = false;
1161
1271
  idleTimeout = null;
1272
+ const deleted = pendingToolCalls.delete(sessionID);
1273
+ if (deleted) logDebug("sessionIdleHook cleared session pendingToolCalls", { sessionID, hadPending: deleted });
1162
1274
  }
1163
1275
  }, 10000);
1164
1276
  };
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { CerebroClient } from "./client.js";
7
- import { autoRecallHook, autocontinueHook, compactingHook, fetchPolicyNudgeHook, keywordDetectionHook, sessionIdleHook } from "./hooks.js";
7
+ import { autoRecallHook, autocontinueHook, compactingHook, fetchPolicyNudgeHook, keywordDetectionHook, sessionIdleHook, soulWhisperToolTracker } from "./hooks.js";
8
8
  import { getUserTag, getProjectTag } from "./tags.js";
9
9
  import { buildTools } from "./tools.js";
10
10
  import { logInfo, logDebug, logError } from "./logger.js";
@@ -119,7 +119,7 @@ const OmemPlugin: Plugin = async (input) => {
119
119
  let mainSessionLocked = false;
120
120
  let cachedAgentName: string | undefined;
121
121
 
122
- const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId);
122
+ const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory);
123
123
 
124
124
  let contextInjectedThisTurn = false;
125
125
 
@@ -147,11 +147,12 @@ const OmemPlugin: Plugin = async (input) => {
147
147
  return wrappedRecallHook(input, output);
148
148
  },
149
149
  "chat.message": keywordDetectionHook(cerebroClient, containerTags, config.ingest.autoCaptureThreshold, tui, config.ingest.ingestMode, config, agentId),
150
- "experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId),
151
- "experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId),
152
- tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId }),
153
- event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }),
154
- "experimental.chat.messages.transform": fetchPolicyNudgeHook(() => contextInjectedThisTurn),
150
+ "experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
151
+ "experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
152
+ tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory }),
153
+ event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }, directory),
154
+ "experimental.chat.messages.transform": fetchPolicyNudgeHook(() => contextInjectedThisTurn, config),
155
+ "tool.execute.before": (() => { const tracker = soulWhisperToolTracker(config); return tracker; })(),
155
156
  "shell.env": async (_input: any, output: any) => {
156
157
  if (directory) {
157
158
  output.env.OMEM_PROJECT_DIR = directory;
package/src/tools.ts CHANGED
@@ -6,6 +6,7 @@ export interface ToolContext {
6
6
  agentId?: string;
7
7
  getSessionId: () => string | undefined;
8
8
  getAgentName?: () => string;
9
+ getProjectPath?: () => string | undefined;
9
10
  }
10
11
 
11
12
  export function buildTools(client: CerebroClient, containerTags: string[], context: ToolContext) {
@@ -16,7 +17,10 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
16
17
  "Use when the user explicitly asks to remember something, " +
17
18
  "or when you identify important preferences, facts, or decisions worth preserving. " +
18
19
  "IMPORTANT: Before calling, you MUST analyze: (1) Which category fits best? (2) Is this project-specific or cross-project? (3) Does it contain sensitive data? (4) Are tags accurate and descriptive? " +
19
- "Every memory MUST have a correct category and at least 1 meaningful tag.",
20
+ "Every memory MUST have a correct category and at least 1 meaningful tag. " +
21
+ "Memories are automatically scoped to the current project via project_path. " +
22
+ "Set scope='global' for cross-project memories that should be visible everywhere. " +
23
+ "Private memories (visibility='private') are always agent-scoped and not bound to any project — use for sensitive data.",
20
24
  args: {
21
25
  content: tool.schema.string().describe(
22
26
  "The information to remember. MUST be: atomic (one fact per memory), complete (self-contained without context), and precise (no ambiguity). " +
@@ -77,6 +81,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
77
81
  context.getSessionId(),
78
82
  args.visibility,
79
83
  args.category,
84
+ context.getProjectPath?.(),
80
85
  );
81
86
  if (!result) return JSON.stringify({ ok: false, error: "The Cerebro server may be unavailable." });
82
87
  return JSON.stringify({ ok: true, id: result.id, tags: result.tags });
@@ -86,7 +91,10 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
86
91
  memory_search: tool({
87
92
  description:
88
93
  "Search the user's long-term memory by semantic similarity. " +
89
- "Use to recall previously stored preferences, facts, or context.",
94
+ "Use to recall previously stored preferences, facts, or context. " +
95
+ "Searches are automatically filtered by the current project_path. " +
96
+ "Global-scope memories and memories without a project_path are always included in results. " +
97
+ "Private memories are visible only to the creating agent.",
90
98
  args: {
91
99
  query: tool.schema.string().describe("Natural-language search query"),
92
100
  limit: tool.schema
@@ -104,6 +112,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
104
112
  args.limit ?? 10,
105
113
  args.scope,
106
114
  containerTags,
115
+ context.getProjectPath?.(),
107
116
  );
108
117
  if (results.length === 0) return JSON.stringify({ ok: true, count: 0, results: [] });
109
118
  const items = results.map((r) => ({
@@ -189,7 +198,9 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
189
198
 
190
199
  memory_ingest: tool({
191
200
  description:
192
- "Ingest conversation messages for intelligent extraction. The system extracts atomic facts, deduplicates, and reconciles with existing memories.",
201
+ "Ingest conversation messages for intelligent extraction. The system extracts atomic facts, deduplicates, and reconciles with existing memories. " +
202
+ "Extracted memories are automatically scoped to the current project via project_path. " +
203
+ "Global-scope memories are visible across all projects.",
193
204
  args: {
194
205
  messages: tool.schema
195
206
  .array(
@@ -219,6 +230,7 @@ export function buildTools(client: CerebroClient, containerTags: string[], conte
219
230
  tags: args.tags,
220
231
  sessionId: args.session_id,
221
232
  agentId: effectiveAgentId,
233
+ projectPath: context.getProjectPath?.(),
222
234
  });
223
235
  if (result === null) return JSON.stringify({ ok: false, error: "Ingestion failed" });
224
236
  return JSON.stringify({ ok: true, result });