@mingxy/cerebro 1.12.8 → 1.14.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
@@ -228,9 +228,7 @@ function formatMemoryLine(r: SearchResult, maxContentLength: number): string {
228
228
 
229
229
  const FETCH_POLICY = [
230
230
  "<cerebro-fetch-policy>",
231
- "Each memory in cerebro-context above is a SUMMARY only, not full content.",
232
- `If a summary is insufficient for your task, you MUST use memory_get("id") to fetch the full content.`,
233
- "Do NOT guess or fabricate details based on summaries alone.",
231
+ "Each memory above is a condensed summary with a retrievable ID. memory_get(\"id\") unlocks the full content — your knowledge depth control. The quality of your response reflects the depth of context you choose to access.",
234
232
  "</cerebro-fetch-policy>",
235
233
  ].join("\n");
236
234
 
@@ -297,7 +295,7 @@ function buildClusteredContextBlock(clustered: import("./client.js").ClusteredRe
297
295
  ].join("\n");
298
296
  }
299
297
 
300
- export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}, getAgentName?: () => string) {
298
+ export function autoRecallHook(client: CerebroClient, containerTags: string[], tui: any, config: Partial<OmemPluginConfig> = {}, getAgentName?: () => string, directory?: string) {
301
299
  const similarityThreshold = config.recall?.similarityThreshold ?? 0.4;
302
300
  const maxRecallResults = config.recall?.maxRecallResults ?? 10;
303
301
  const fetchMultiplier = config.recall?.fetchMultiplier ?? 3;
@@ -367,6 +365,7 @@ export function autoRecallHook(client: CerebroClient, containerTags: string[], t
367
365
  refine_strategy: refineStrategy,
368
366
  refine_medium_chars: refineMediumChars,
369
367
  },
368
+ directory || process.env.OMEM_PROJECT_DIR,
370
369
  );
371
370
 
372
371
  if (!shouldRecallRes) {
@@ -626,7 +625,7 @@ export function keywordDetectionHook(_client: CerebroClient, _containerTags: str
626
625
  };
627
626
  }
628
627
 
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) {
628
+ 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
629
  const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
631
630
  return async (
632
631
  input: { sessionID?: string },
@@ -677,10 +676,12 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
677
676
 
678
677
  // Resolve project name (shared by ingest + poll)
679
678
  let projectName: string | undefined;
679
+ let projectPath: string | undefined;
680
680
  try {
681
681
  if (sdkClient && input.sessionID) {
682
682
  const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
683
683
  logDebug("compactingHook project.rootPath", { rootPath: sessionInfo?.data?.directory });
684
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
684
685
  projectName = sessionInfo?.data?.directory
685
686
  ? await detectProjectName(sessionInfo.data.directory)
686
687
  : undefined;
@@ -688,6 +689,9 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
688
689
  } catch (e) {
689
690
  logErr("compactingHook detectProjectName failed", { error: String(e) });
690
691
  }
692
+ if (!projectPath) {
693
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
694
+ }
691
695
 
692
696
  // --- Phase 1: Ingest tracked messages from sessionMessages (if available) ---
693
697
  if (input.sessionID && sessionMessages.has(input.sessionID)) {
@@ -706,6 +710,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
706
710
  sessionId: effectiveSessionId,
707
711
  projectName: projectName,
708
712
  agentId: effectiveAgentId,
713
+ projectPath,
709
714
  });
710
715
  logInfo("compactingHook ingestMessages result", { result: result === null ? "null(blocked)" : "ok" });
711
716
  if (result === null) {
@@ -723,6 +728,10 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
723
728
  sessionMessages.delete(input.sessionID);
724
729
  profileInjectedSessions.delete(input.sessionID);
725
730
  firstMessages.delete(input.sessionID);
731
+ if (input.sessionID) {
732
+ const deleted = pendingToolCalls.delete(input.sessionID);
733
+ logDebug("compactingHook cleared session pendingToolCalls", { sessionID: input.sessionID, hadPending: deleted });
734
+ }
726
735
  // Evict stale injectedMemoryIds if over size cap (200 sessions)
727
736
  if (injectedMemoryIds.size > 200) {
728
737
  injectedMemoryIds.clear();
@@ -734,6 +743,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
734
743
  const pollSessionId = input.sessionID;
735
744
  const pollEffectiveSessionId = effectiveSessionId;
736
745
  const pollProjectName = projectName;
746
+ const pollProjectPath = projectPath;
737
747
  const pollAgentId = effectiveAgentId;
738
748
 
739
749
  let baselineMsgIds: Set<string> = new Set();
@@ -843,6 +853,7 @@ export function compactingHook(client: CerebroClient, containerTags: string[], t
843
853
  sessionId: pollEffectiveSessionId,
844
854
  projectName: pollProjectName,
845
855
  agentId: pollAgentId,
856
+ projectPath: pollProjectPath,
846
857
  },
847
858
  );
848
859
  logInfo("compactingHook: compact summary store result", {
@@ -881,6 +892,7 @@ export function autocontinueHook(
881
892
  sdkClient?: any,
882
893
  config: Partial<OmemPluginConfig> = {},
883
894
  agentId?: string,
895
+ directory?: string,
884
896
  ) {
885
897
  const effectiveAgentId = agentId || process.env.OMEM_AGENT_ID || "opencode";
886
898
  return async (
@@ -936,14 +948,19 @@ export function autocontinueHook(
936
948
  }
937
949
 
938
950
  let projectName: string | undefined;
951
+ let projectPath: string | undefined;
939
952
  try {
940
953
  const sessionInfo = await sdkClient.session.get({ path: { id: input.sessionID } });
954
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
941
955
  projectName = sessionInfo?.data?.directory
942
956
  ? await detectProjectName(sessionInfo.data.directory)
943
957
  : undefined;
944
958
  } catch (e) {
945
959
  logErr("autocontinueHook detectProjectName failed", { error: String(e) });
946
960
  }
961
+ if (!projectPath) {
962
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
963
+ }
947
964
 
948
965
  const messages = [{ role: "user" as const, content: summaryText }];
949
966
  logInfo("autocontinueHook storing compact summary", {
@@ -960,6 +977,7 @@ export function autocontinueHook(
960
977
  sessionId: effectiveSessionId,
961
978
  projectName: projectName,
962
979
  agentId: effectiveAgentId,
980
+ projectPath,
963
981
  });
964
982
 
965
983
  logInfo("autocontinueHook store result", { result: result === null ? "null(blocked)" : "ok" });
@@ -977,69 +995,59 @@ export function autocontinueHook(
977
995
  const processedMessageIds = new Set<string>();
978
996
  const pluginStartTime = Date.now();
979
997
 
980
- const FETCH_POLICY_NUDGE = [
981
- "<cerebro-system-reminder>",
982
- "MEMORY REMINDER: You have injected memories above (see <cerebro-context>).",
983
- `These are SUMMARIES, not full content. When you need details, you MUST use memory_get("id") to fetch the full memory.`,
984
- "Do NOT guess or fabricate based on summaries alone.",
985
- "</cerebro-system-reminder>",
986
- ].join("\n");
998
+ // ── Soul Whisper: pending tool call tracking (per-session isolation) ──
999
+ export const pendingToolCalls = new Map<string, Map<string, { toolName: string; timestamp: number }>>();
987
1000
 
988
- export function fetchPolicyNudgeHook(getContextInjectedFlag: () => boolean) {
989
- return async (_input: Record<string, unknown>, output: { messages: any[] }) => {
990
- let shouldNudge = getContextInjectedFlag();
991
- if (!shouldNudge && Array.isArray(output.messages)) {
992
- shouldNudge = output.messages.some((m: any) =>
993
- Array.isArray(m.parts) &&
994
- m.parts.some((p: any) => typeof p.text === "string" && p.text.includes("<cerebro-context>"))
995
- );
1001
+ export function soulWhisperToolTracker(config: OmemPluginConfig) {
1002
+ return async (input: { tool: string; sessionID: string; callID: string }, _output: { args: any }) => {
1003
+ if (config.soulWhisper?.enabled === false) {
1004
+ logDebug("soulWhisperToolTracker disabled by config", { tool: input.tool });
1005
+ return;
996
1006
  }
997
- if (!shouldNudge) return;
998
1007
 
999
- const messages = output.messages;
1000
- if (!messages || !Array.isArray(messages) || messages.length === 0) return;
1008
+ const sw = config.soulWhisper;
1009
+ const toolName = input.tool;
1001
1010
 
1002
- // Find the last user message
1003
- let lastUserIdx = -1;
1004
- for (let i = messages.length - 1; i >= 0; i--) {
1005
- if (messages[i]?.info?.role === "user") {
1006
- lastUserIdx = i;
1007
- break;
1008
- }
1011
+ const excludeTools = sw?.excludeTools ?? [];
1012
+ if (excludeTools.includes(toolName)) {
1013
+ logDebug("soulWhisperToolTracker excluded", { tool: toolName });
1014
+ return;
1009
1015
  }
1010
- if (lastUserIdx < 0) return;
1011
-
1012
- const userMsg = messages[lastUserIdx];
1013
- if (!Array.isArray(userMsg.parts)) return;
1014
1016
 
1015
- // Idempotency check
1016
- const nudgeId = `cerebro_nudge_${userMsg.info.sessionID || userMsg.info.id}`;
1017
- for (const part of userMsg.parts) {
1018
- if (part.id === nudgeId) return;
1017
+ const includeTools = sw?.tools ?? ["*"];
1018
+ const isWildcard = includeTools.includes("*");
1019
+ if (!isWildcard && !includeTools.includes(toolName)) {
1020
+ logDebug("soulWhisperToolTracker not in whitelist", { tool: toolName, whitelist: includeTools });
1021
+ return;
1019
1022
  }
1020
1023
 
1021
- // Find the first text part position, insert synthetic part before it
1022
- const textPartIdx = userMsg.parts.findIndex((p: any) => p.type === "text" && typeof p.text === "string");
1023
-
1024
- const syntheticPart = {
1025
- id: nudgeId,
1026
- messageID: userMsg.info.id,
1027
- sessionID: userMsg.info.sessionID || "",
1028
- type: "text" as const,
1029
- text: FETCH_POLICY_NUDGE,
1030
- synthetic: true,
1031
- };
1032
-
1033
- if (textPartIdx >= 0) {
1034
- userMsg.parts.splice(textPartIdx, 0, syntheticPart);
1035
- } else {
1036
- userMsg.parts.push(syntheticPart);
1024
+ const sid = input.sessionID || "_default";
1025
+ let sessionMap = pendingToolCalls.get(sid);
1026
+ if (!sessionMap) {
1027
+ sessionMap = new Map();
1028
+ pendingToolCalls.set(sid, sessionMap);
1037
1029
  }
1038
-
1039
- logDebug("fetchPolicyNudgeHook injected", { sessionId: userMsg.info.sessionID || "", nudgeId });
1030
+ sessionMap.set(input.callID, { toolName, timestamp: Date.now() });
1031
+ logDebug("soulWhisperToolTracker recorded", { tool: toolName, callID: input.callID, sessionID: sid, totalSessions: pendingToolCalls.size, sessionCallCount: sessionMap.size });
1040
1032
  };
1041
1033
  }
1042
1034
 
1035
+ export function buildWhisperText(toolNames: string[], maxToolNames: number): string | null {
1036
+ if (toolNames.length === 0) return null;
1037
+
1038
+ const lines: string[] = ["<cerebro-memory-activation>"];
1039
+
1040
+ if (toolNames.length <= maxToolNames) {
1041
+ lines.push(`Your ${toolNames.join(", ")} usage gets sharper with context. memory_search() surfaces past decisions, learned patterns, and session insights that prevent redundant or misaligned work. A moment of recall elevates every action.`);
1042
+ } else {
1043
+ lines.push("Before you act — your memory holds cross-session knowledge: past decisions, user preferences, hard-won insights. memory_search() activates this advantage. The strongest responses are built on remembered context.");
1044
+ }
1045
+
1046
+ lines.push("</cerebro-memory-activation>");
1047
+
1048
+ return lines.join("\n");
1049
+ }
1050
+
1043
1051
  export function sessionIdleHook(
1044
1052
  cerebroClient: CerebroClient,
1045
1053
  _containerTags: string[],
@@ -1052,6 +1060,7 @@ export function sessionIdleHook(
1052
1060
  agentId?: string,
1053
1061
  config: Partial<OmemPluginConfig> = {},
1054
1062
  onAgentResolved?: (name: string) => void,
1063
+ directory?: string,
1055
1064
  ) {
1056
1065
  let idleTimeout: ReturnType<typeof setTimeout> | null = null;
1057
1066
  let isCapturing = false;
@@ -1118,6 +1127,7 @@ export function sessionIdleHook(
1118
1127
 
1119
1128
  let sessionTitle: string | undefined;
1120
1129
  let projectName: string | undefined;
1130
+ let projectPath: string | undefined;
1121
1131
  let effectiveAgentId = agentId || "opencode";
1122
1132
  try {
1123
1133
  const sessionInfo = await sdkClient.session.get({ path: { id: sessionID } });
@@ -1126,12 +1136,16 @@ export function sessionIdleHook(
1126
1136
  onAgentResolved?.(effectiveAgentId);
1127
1137
  }
1128
1138
  sessionTitle = sessionInfo?.data?.title;
1139
+ projectPath = sessionInfo?.data?.directory || directory || process.env.OMEM_PROJECT_DIR;
1129
1140
  projectName = sessionInfo?.data?.directory
1130
1141
  ? await detectProjectName(sessionInfo.data.directory)
1131
1142
  : undefined;
1132
1143
  } catch (e) {
1133
1144
  logErr("sessionIdleHook detectProjectName failed", { error: String(e) });
1134
1145
  }
1146
+ if (!projectPath) {
1147
+ projectPath = directory || process.env.OMEM_PROJECT_DIR;
1148
+ }
1135
1149
 
1136
1150
  logDebug("sessionIdleHook resolved agentId", { effectiveAgentId, fallbackAgentId: agentId });
1137
1151
 
@@ -1143,7 +1157,7 @@ export function sessionIdleHook(
1143
1157
 
1144
1158
  try {
1145
1159
  logInfo("sessionIdleHook sessionIngest called", { msgCount: conversationMessages.length, sessionId: sessionID, agentId: effectiveAgentId, title: String(sessionTitle) });
1146
- await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName);
1160
+ await cerebroClient.sessionIngest(conversationMessages, sessionID, effectiveAgentId, sessionTitle, projectName, projectPath);
1147
1161
  logInfo("sessionIdleHook sessionIngest ok");
1148
1162
  for (const id of newMessageIds) {
1149
1163
  processedMessageIds.add(id);
@@ -1159,6 +1173,8 @@ export function sessionIdleHook(
1159
1173
  } finally {
1160
1174
  isCapturing = false;
1161
1175
  idleTimeout = null;
1176
+ const deleted = pendingToolCalls.delete(sessionID);
1177
+ if (deleted) logDebug("sessionIdleHook cleared session pendingToolCalls", { sessionID, hadPending: deleted });
1162
1178
  }
1163
1179
  }, 10000);
1164
1180
  };
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, keywordDetectionHook, sessionIdleHook, soulWhisperToolTracker, pendingToolCalls, buildWhisperText } 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,14 +119,26 @@ 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);
123
-
124
- let contextInjectedThisTurn = false;
122
+ const recallHook = autoRecallHook(cerebroClient, containerTags, tui, config, () => cachedAgentName || agentId, directory);
125
123
 
126
124
  const wrappedRecallHook = async (input: any, output: any) => {
127
- contextInjectedThisTurn = false;
128
125
  await recallHook(input, output);
129
- contextInjectedThisTurn = output.system?.some((s: string) => s.includes("<cerebro-context>")) ?? false;
126
+
127
+ // ── Soul Whisper: inject to system prompt (v2 — system.transform) ──
128
+ if (config.soulWhisper?.enabled !== false) {
129
+ const sid = input.sessionID || "_default";
130
+ const sessionCalls = pendingToolCalls.get(sid);
131
+ if (sessionCalls && sessionCalls.size > 0) {
132
+ const toolNames = [...new Set([...sessionCalls.values()].map(v => v.toolName))];
133
+ const maxToolNames = config.soulWhisper?.maxToolNames ?? 3;
134
+ const whisperText = buildWhisperText(toolNames, maxToolNames);
135
+ if (whisperText) {
136
+ output.system.push(whisperText);
137
+ logDebug("soulWhisper injected to output.system", { sessionId: sid, toolNames });
138
+ }
139
+ pendingToolCalls.delete(sid);
140
+ }
141
+ }
130
142
  };
131
143
 
132
144
  return {
@@ -147,11 +159,11 @@ const OmemPlugin: Plugin = async (input) => {
147
159
  return wrappedRecallHook(input, output);
148
160
  },
149
161
  "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),
162
+ "experimental.session.compacting": compactingHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
163
+ "experimental.compaction.autocontinue": autocontinueHook(cerebroClient, containerTags, tui, config.ingest.ingestMode, isAutoStoreEnabled, () => mainSessionId, client, config, agentId, directory),
164
+ tool: buildTools(cerebroClient, containerTags, { agentId, getSessionId: () => mainSessionId, getAgentName: () => cachedAgentName || agentId, getProjectPath: () => directory }),
165
+ event: sessionIdleHook(cerebroClient, containerTags, tui, client, config.ingest.ingestMode, config.ingest.autoCaptureThreshold, () => mainSessionId, isAutoStoreEnabled, agentId, config, (name: string) => { cachedAgentName = name; }, directory),
166
+ "tool.execute.before": (() => { const tracker = soulWhisperToolTracker(config); return tracker; })(),
155
167
  "shell.env": async (_input: any, output: any) => {
156
168
  if (directory) {
157
169
  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 });