From 46d6b119b6f1c7382c00ae215a3b0c6ac687639a Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Mon, 30 Mar 2026 18:32:15 -0700 Subject: [PATCH] feat(core): add project-level memory scope to save_memory tool (#24161) --- packages/cli/src/ui/AppContainer.tsx | 1 + packages/core/src/config/config.test.ts | 3 + packages/core/src/config/config.ts | 18 +++- packages/core/src/config/memory.ts | 7 ++ packages/core/src/config/storage.test.ts | 11 ++ packages/core/src/config/storage.ts | 5 + .../core/__snapshots__/prompts.test.ts.snap | 95 ++++++++++++---- .../prompts/snippets-memory-manager.test.ts | 2 +- packages/core/src/prompts/snippets.legacy.ts | 5 + packages/core/src/prompts/snippets.ts | 12 ++- .../core/src/services/contextManager.test.ts | 7 ++ packages/core/src/services/contextManager.ts | 27 ++++- .../coreToolsModelSnapshots.test.ts.snap | 30 ++++-- .../tools/definitions/base-declarations.ts | 1 + .../model-family-sets/default-legacy.ts | 17 ++- .../definitions/model-family-sets/gemini-3.ts | 11 +- packages/core/src/tools/memoryTool.test.ts | 101 +++++++++++++++++- packages/core/src/tools/memoryTool.ts | 60 ++++++++--- packages/core/src/utils/memoryDiscovery.ts | 32 +++++- 19 files changed, 382 insertions(+), 63 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 129bcb8fda..4da8acfdb7 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1027,6 +1027,7 @@ Logging in with Google... Restarting Gemini CLI to continue. if (config.isJitContextEnabled()) { await config.getContextManager()?.refresh(); + config.updateSystemInstructionIfInitialized(); flattenedMemory = flattenMemory(config.getUserMemory()); fileCount = config.getGeminiMdFileCount(); } else { diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 14ac3b7cf1..59133f6997 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -227,6 +227,7 @@ vi.mock('../services/contextManager.js', () => ({ getGlobalMemory: vi.fn().mockReturnValue(''), getExtensionMemory: vi.fn().mockReturnValue(''), getEnvironmentMemory: vi.fn().mockReturnValue(''), + getUserProjectMemory: vi.fn().mockReturnValue(''), getLoadedPaths: vi.fn().mockReturnValue(new Set()), })), })); @@ -2948,6 +2949,7 @@ describe('Config JIT Initialization', () => { getEnvironmentMemory: vi .fn() .mockReturnValue('Environment Memory\n\nMCP Instructions'), + getUserProjectMemory: vi.fn().mockReturnValue(''), getLoadedPaths: vi.fn().mockReturnValue(new Set(['/path/to/GEMINI.md'])), } as unknown as ContextManager; (ContextManager as unknown as Mock).mockImplementation( @@ -2975,6 +2977,7 @@ describe('Config JIT Initialization', () => { global: 'Global Memory', extension: 'Extension Memory', project: 'Environment Memory\n\nMCP Instructions', + userProjectMemory: '', }); // Tier 1: system instruction gets only global memory diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index ec39016933..f3e02510ed 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -2282,6 +2282,7 @@ export class Config implements McpContext, AgentLoopContext { global: this.contextManager.getGlobalMemory(), extension: this.contextManager.getExtensionMemory(), project: this.contextManager.getEnvironmentMemory(), + userProjectMemory: this.contextManager.getUserProjectMemory(), }; } return this.userMemory; @@ -2311,13 +2312,20 @@ export class Config implements McpContext, AgentLoopContext { /** * Returns memory for the system instruction. - * When JIT is enabled, only global memory (Tier 1) goes in the system - * instruction. Extension and project memory (Tier 2) are placed in the - * first user message instead, per the tiered context model. + * When JIT is enabled, global memory and user project memory (Tier 1) go + * in the system instruction. Extension and project memory (Tier 2) are + * placed in the first user message instead, per the tiered context model. + * User project memory is in Tier 1 so mid-session saves are reflected + * via system instruction updates. */ getSystemInstructionMemory(): string | HierarchicalMemory { if (this.experimentalJitContext && this.contextManager) { - return this.contextManager.getGlobalMemory(); + const global = this.contextManager.getGlobalMemory(); + const userProjectMemory = this.contextManager.getUserProjectMemory(); + if (userProjectMemory?.trim()) { + return { global, userProjectMemory }; + } + return global; } return this.userMemory; } @@ -3476,7 +3484,7 @@ export class Config implements McpContext, AgentLoopContext { ); if (!this.isMemoryManagerEnabled()) { maybeRegister(MemoryTool, () => - registry.registerTool(new MemoryTool(this.messageBus)), + registry.registerTool(new MemoryTool(this.messageBus, this.storage)), ); } maybeRegister(WebSearchTool, () => diff --git a/packages/core/src/config/memory.ts b/packages/core/src/config/memory.ts index 6ae902d5c6..146e38d0a6 100644 --- a/packages/core/src/config/memory.ts +++ b/packages/core/src/config/memory.ts @@ -8,6 +8,7 @@ export interface HierarchicalMemory { global?: string; extension?: string; project?: string; + userProjectMemory?: string; } /** @@ -21,6 +22,12 @@ export function flattenMemory(memory?: string | HierarchicalMemory): string { if (memory.global?.trim()) { sections.push({ name: 'Global', content: memory.global.trim() }); } + if (memory.userProjectMemory?.trim()) { + sections.push({ + name: 'User Project Memory', + content: memory.userProjectMemory.trim(), + }); + } if (memory.extension?.trim()) { sections.push({ name: 'Extension', content: memory.extension.trim() }); } diff --git a/packages/core/src/config/storage.test.ts b/packages/core/src/config/storage.test.ts index ea8fce6da3..b5b8c26841 100644 --- a/packages/core/src/config/storage.test.ts +++ b/packages/core/src/config/storage.test.ts @@ -147,6 +147,17 @@ describe('Storage – additional helpers', () => { expect(storage.getProjectAgentsDir()).toBe(expected); }); + it('getProjectMemoryDir returns ~/.gemini/memory/', async () => { + await storage.initialize(); + const expected = path.join( + os.homedir(), + GEMINI_DIR, + 'memory', + PROJECT_SLUG, + ); + expect(storage.getProjectMemoryDir()).toBe(expected); + }); + it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => { const expected = path.join( os.homedir(), diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 38654346fa..cfbe6cf945 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -266,6 +266,11 @@ export class Storage { return path.join(historyDir, identifier); } + getProjectMemoryDir(): string { + const identifier = this.getProjectIdentifier(); + return path.join(Storage.getGlobalGeminiDir(), 'memory', identifier); + } + getWorkspaceSettingsPath(): string { return path.join(this.getGeminiDir(), 'settings.json'); } diff --git a/packages/core/src/core/__snapshots__/prompts.test.ts.snap b/packages/core/src/core/__snapshots__/prompts.test.ts.snap index 51f9a9e59e..b4e8dd4e7e 100644 --- a/packages/core/src/core/__snapshots__/prompts.test.ts.snap +++ b/packages/core/src/core/__snapshots__/prompts.test.ts.snap @@ -164,7 +164,10 @@ Use the \`exit_plan_mode\` tool to present the plan and formally request approva - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -342,7 +345,10 @@ An approved plan is available for this task at \`/tmp/plans/feature-x.md\`. - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -627,7 +633,10 @@ Use the \`exit_plan_mode\` tool to present the plan and formally request approva - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -782,7 +791,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -923,7 +935,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -1047,7 +1062,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -1688,7 +1706,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -1843,7 +1864,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2002,7 +2026,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2161,7 +2188,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2316,7 +2346,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2465,7 +2498,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2619,7 +2655,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -2774,7 +2813,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -3064,7 +3106,10 @@ You are operating with a persistent file-based task tracking system located at \ - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -3460,7 +3505,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -3615,7 +3663,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -3882,7 +3933,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -4037,7 +4091,10 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi - **Command Execution:** Use the \`run_shell_command\` tool for running shell commands, remembering the safety rule to explain modifying commands first. - **Background Processes:** To run a command in the background, set the \`is_background\` parameter to true. If unsure, ask the user. - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim). If you choose to execute an interactive command consider letting the user know they can press \`tab\` to focus into the shell to provide input. -- **Memory Tool:** Use \`save_memory\` only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only. If unsure whether a fact is worth remembering globally, ask the user. +- **Memory Tool:** Use \`save_memory\` to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task. If unsure whether a fact is global or project-specific, ask the user. - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details diff --git a/packages/core/src/prompts/snippets-memory-manager.test.ts b/packages/core/src/prompts/snippets-memory-manager.test.ts index 070e49f8c0..19aa8f478b 100644 --- a/packages/core/src/prompts/snippets-memory-manager.test.ts +++ b/packages/core/src/prompts/snippets-memory-manager.test.ts @@ -18,7 +18,7 @@ describe('renderOperationalGuidelines - memoryManagerEnabled', () => { it('should include standard memory tool guidance when memoryManagerEnabled is false', () => { const result = renderOperationalGuidelines(baseOptions); expect(result).toContain('save_memory'); - expect(result).toContain('persistent user-related information'); + expect(result).toContain('persist facts across sessions'); expect(result).not.toContain('subagent'); }); diff --git a/packages/core/src/prompts/snippets.legacy.ts b/packages/core/src/prompts/snippets.legacy.ts index ebe08847ed..0367596c69 100644 --- a/packages/core/src/prompts/snippets.legacy.ts +++ b/packages/core/src/prompts/snippets.legacy.ts @@ -405,6 +405,11 @@ ${trimmed} `\n${memory.global.trim()}\n`, ); } + if (memory.userProjectMemory?.trim()) { + sections.push( + `\n--- User's Project Memory (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End User's Project Memory ---\n`, + ); + } if (memory.extension?.trim()) { sections.push( `\n${memory.extension.trim()}\n`, diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index a16ef59461..d7e95a1f4e 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -517,6 +517,11 @@ ${trimmed} `\n${memory.global.trim()}\n`, ); } + if (memory.userProjectMemory?.trim()) { + sections.push( + `\n--- User's Project Memory (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End User's Project Memory ---\n`, + ); + } if (memory.extension?.trim()) { sections.push( `\n${memory.extension.trim()}\n`, @@ -798,9 +803,12 @@ function toolUsageRememberingFacts( - **Memory Tool:** You MUST use ${formatToolName(MEMORY_TOOL_NAME)} to proactively record facts, preferences, and workflows that apply across all sessions. Whenever the user explicitly tells you to "remember" something, or when they state a preference or workflow (like "always lint after editing"), you MUST immediately call the save_memory subagent. Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is strictly for persistent general knowledge.`; } const base = ` -- **Memory Tool:** Use ${formatToolName(MEMORY_TOOL_NAME)} only for global user preferences, personal facts, or high-level information that applies across all sessions. Never save workspace-specific context, local file paths, or transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is for persistent user-related information only.`; +- **Memory Tool:** Use ${formatToolName(MEMORY_TOOL_NAME)} to persist facts across sessions. It supports two scopes via the \`scope\` parameter: + - \`"global"\` (default): Cross-project preferences and personal facts loaded in every workspace. + - \`"project"\`: Facts specific to the current workspace, private to the user (not committed to the repo). Use this for local dev setup notes, project-specific workflows, or personal reminders about this codebase. + Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task.`; const suffix = options.interactive - ? ' If unsure whether a fact is worth remembering globally, ask the user.' + ? ' If unsure whether a fact is global or project-specific, ask the user.' : ''; return base + suffix; } diff --git a/packages/core/src/services/contextManager.test.ts b/packages/core/src/services/contextManager.test.ts index a6a3c8cd0f..3d06e2485d 100644 --- a/packages/core/src/services/contextManager.test.ts +++ b/packages/core/src/services/contextManager.test.ts @@ -17,6 +17,7 @@ vi.mock('../utils/memoryDiscovery.js', async (importOriginal) => { return { ...actual, getGlobalMemoryPaths: vi.fn(), + getUserProjectMemoryPaths: vi.fn(), getExtensionMemoryPaths: vi.fn(), getEnvironmentMemoryPaths: vi.fn(), readGeminiMdFiles: vi.fn(), @@ -47,12 +48,18 @@ describe('ContextManager', () => { }), isTrustedFolder: vi.fn().mockReturnValue(true), getMemoryBoundaryMarkers: vi.fn().mockReturnValue(['.git']), + storage: { + getProjectMemoryDir: vi + .fn() + .mockReturnValue('/home/user/.gemini/memory/test-project'), + }, } as unknown as Config; contextManager = new ContextManager(mockConfig); vi.clearAllMocks(); vi.spyOn(coreEvents, 'emit'); vi.mocked(memoryDiscovery.getExtensionMemoryPaths).mockReturnValue([]); + vi.mocked(memoryDiscovery.getUserProjectMemoryPaths).mockResolvedValue([]); // default mock: deduplication returns paths as-is (no deduplication) vi.mocked( memoryDiscovery.deduplicatePathsByFileIdentity, diff --git a/packages/core/src/services/contextManager.ts b/packages/core/src/services/contextManager.ts index 3d7400c747..43ae627796 100644 --- a/packages/core/src/services/contextManager.ts +++ b/packages/core/src/services/contextManager.ts @@ -8,6 +8,7 @@ import { loadJitSubdirectoryMemory, concatenateInstructions, getGlobalMemoryPaths, + getUserProjectMemoryPaths, getExtensionMemoryPaths, getEnvironmentMemoryPaths, readGeminiMdFiles, @@ -25,6 +26,7 @@ export class ContextManager { private globalMemory: string = ''; private extensionMemory: string = ''; private projectMemory: string = ''; + private userProjectMemoryContent: string = ''; constructor(config: Config) { this.config = config; @@ -45,7 +47,7 @@ export class ContextManager { } private async discoverMemoryPaths() { - const [global, extension, project] = await Promise.all([ + const [global, extension, project, userProjectMemory] = await Promise.all([ getGlobalMemoryPaths(), Promise.resolve( getExtensionMemoryPaths(this.config.getExtensionLoader()), @@ -56,18 +58,25 @@ export class ContextManager { this.config.getMemoryBoundaryMarkers(), ) : Promise.resolve([]), + getUserProjectMemoryPaths(this.config.storage.getProjectMemoryDir()), ]); - return { global, extension, project }; + return { global, extension, project, userProjectMemory }; } private async loadMemoryContents(paths: { global: string[]; extension: string[]; project: string[]; + userProjectMemory: string[]; }) { const allPathsStringDeduped = Array.from( - new Set([...paths.global, ...paths.extension, ...paths.project]), + new Set([ + ...paths.global, + ...paths.extension, + ...paths.project, + ...paths.userProjectMemory, + ]), ); // deduplicate by file identity to handle case-insensitive filesystems @@ -97,13 +106,19 @@ export class ContextManager { } private categorizeMemoryContents( - paths: { global: string[]; extension: string[]; project: string[] }, + paths: { + global: string[]; + extension: string[]; + project: string[]; + userProjectMemory: string[]; + }, contentsMap: Map, ) { const hierarchicalMemory = categorizeAndConcatenate(paths, contentsMap); this.globalMemory = hierarchicalMemory.global || ''; this.extensionMemory = hierarchicalMemory.extension || ''; + this.userProjectMemoryContent = hierarchicalMemory.userProjectMemory || ''; const mcpInstructions = this.config.getMcpClientManager()?.getMcpInstructions() || ''; @@ -174,6 +189,10 @@ export class ContextManager { return this.projectMemory; } + getUserProjectMemory(): string { + return this.userProjectMemoryContent; + } + private markAsLoaded(paths: string[]): void { paths.forEach((p) => this.loadedPaths.add(p)); } diff --git a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap index 5a8291bcfc..dbaad2d1f8 100644 --- a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap +++ b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap @@ -640,13 +640,13 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snapshot for tool: save_memory 1`] = ` { "description": " -Saves concise global user context (preferences, facts) for use across ALL workspaces. +Saves concise user context (preferences, facts) for use across future sessions. -### CRITICAL: GLOBAL CONTEXT ONLY -NEVER save workspace-specific context, local paths, or commands (e.g. "The entry point is src/index.js", "The test command is npm test"). These are local to the current workspace and must NOT be saved globally. EXCLUSIVELY for context relevant across ALL workspaces. +Supports two scopes: +- **global** (default): Cross-project preferences loaded in every workspace. Use for "Remember X" or clear personal facts. +- **project**: Facts specific to the current workspace, private to the user (not committed to the repo). Use for local dev setup notes, project-specific workflows, or personal reminders about this codebase. -- Use for "Remember X" or clear personal facts. -- Do NOT use for session context.", +Do NOT use for session-specific context or temporary data.", "name": "save_memory", "parametersJsonSchema": { "additionalProperties": false, @@ -655,6 +655,14 @@ NEVER save workspace-specific context, local paths, or commands (e.g. "The entry "description": "The specific fact or piece of information to remember. Should be a clear, self-contained statement.", "type": "string", }, + "scope": { + "description": "Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.", + "enum": [ + "global", + "project", + ], + "type": "string", + }, }, "required": [ "fact", @@ -1433,13 +1441,21 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > snapshot for tool: save_memory 1`] = ` { - "description": "Persists global preferences or facts across ALL future sessions. Use this for recurring instructions like coding styles or tool aliases. Unlike 'write_file', which is for project-specific files, this appends to a global memory file loaded in every workspace. If you are unsure whether a fact should be remembered globally, ask the user first. CRITICAL: Do not use for session-specific context or temporary data.", + "description": "Persists preferences or facts across ALL future sessions. Supports two scopes: 'global' (default) for cross-project preferences loaded in every workspace, and 'project' for facts specific to the current workspace that are private to the user (not committed to the repo). Use 'project' scope for things like local dev setup notes, project-specific workflows, or personal reminders about this codebase. CRITICAL: Do not use for session-specific context or temporary data.", "name": "save_memory", "parametersJsonSchema": { "additionalProperties": false, "properties": { "fact": { - "description": "A concise, global fact or preference (e.g., 'I prefer using tabs'). Do not include local paths or project-specific names.", + "description": "A concise fact or preference to remember. Should be a clear, self-contained statement.", + "type": "string", + }, + "scope": { + "description": "Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.", + "enum": [ + "global", + "project", + ], "type": "string", }, }, diff --git a/packages/core/src/tools/definitions/base-declarations.ts b/packages/core/src/tools/definitions/base-declarations.ts index 08b14ce6cb..13f31aa2bb 100644 --- a/packages/core/src/tools/definitions/base-declarations.ts +++ b/packages/core/src/tools/definitions/base-declarations.ts @@ -92,6 +92,7 @@ export const READ_MANY_PARAM_USE_DEFAULT_EXCLUDES = 'useDefaultExcludes'; // -- save_memory -- export const MEMORY_TOOL_NAME = 'save_memory'; export const MEMORY_PARAM_FACT = 'fact'; +export const MEMORY_PARAM_SCOPE = 'scope'; // -- get_internal_docs -- export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs'; diff --git a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts index cd79694f78..dcf9e6e86e 100644 --- a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts +++ b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts @@ -59,6 +59,7 @@ import { READ_MANY_PARAM_RECURSIVE, READ_MANY_PARAM_USE_DEFAULT_EXCLUDES, MEMORY_PARAM_FACT, + MEMORY_PARAM_SCOPE, TODOS_PARAM_TODOS, TODOS_ITEM_PARAM_DESCRIPTION, TODOS_ITEM_PARAM_STATUS, @@ -513,13 +514,13 @@ Use this tool when the user's query implies needing the content of several files save_memory: { name: MEMORY_TOOL_NAME, description: ` -Saves concise global user context (preferences, facts) for use across ALL workspaces. +Saves concise user context (preferences, facts) for use across future sessions. -### CRITICAL: GLOBAL CONTEXT ONLY -NEVER save workspace-specific context, local paths, or commands (e.g. "The entry point is src/index.js", "The test command is npm test"). These are local to the current workspace and must NOT be saved globally. EXCLUSIVELY for context relevant across ALL workspaces. +Supports two scopes: +- **global** (default): Cross-project preferences loaded in every workspace. Use for "Remember X" or clear personal facts. +- **project**: Facts specific to the current workspace, private to the user (not committed to the repo). Use for local dev setup notes, project-specific workflows, or personal reminders about this codebase. -- Use for "Remember X" or clear personal facts. -- Do NOT use for session context.`, +Do NOT use for session-specific context or temporary data.`, parametersJsonSchema: { type: 'object', properties: { @@ -528,6 +529,12 @@ NEVER save workspace-specific context, local paths, or commands (e.g. "The entry description: 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.', }, + [MEMORY_PARAM_SCOPE]: { + type: 'string', + enum: ['global', 'project'], + description: + "Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.", + }, }, required: [MEMORY_PARAM_FACT], additionalProperties: false, diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index b19c157f22..b69ca43e5a 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -59,6 +59,7 @@ import { READ_MANY_PARAM_RECURSIVE, READ_MANY_PARAM_USE_DEFAULT_EXCLUDES, MEMORY_PARAM_FACT, + MEMORY_PARAM_SCOPE, TODOS_PARAM_TODOS, TODOS_ITEM_PARAM_DESCRIPTION, TODOS_ITEM_PARAM_STATUS, @@ -495,14 +496,20 @@ Use this tool when the user's query implies needing the content of several files save_memory: { name: MEMORY_TOOL_NAME, - description: `Persists global preferences or facts across ALL future sessions. Use this for recurring instructions like coding styles or tool aliases. Unlike '${WRITE_FILE_TOOL_NAME}', which is for project-specific files, this appends to a global memory file loaded in every workspace. If you are unsure whether a fact should be remembered globally, ask the user first. CRITICAL: Do not use for session-specific context or temporary data.`, + description: `Persists preferences or facts across ALL future sessions. Supports two scopes: 'global' (default) for cross-project preferences loaded in every workspace, and 'project' for facts specific to the current workspace that are private to the user (not committed to the repo). Use 'project' scope for things like local dev setup notes, project-specific workflows, or personal reminders about this codebase. CRITICAL: Do not use for session-specific context or temporary data.`, parametersJsonSchema: { type: 'object', properties: { [MEMORY_PARAM_FACT]: { type: 'string', description: - "A concise, global fact or preference (e.g., 'I prefer using tabs'). Do not include local paths or project-specific names.", + 'A concise fact or preference to remember. Should be a clear, self-contained statement.', + }, + [MEMORY_PARAM_SCOPE]: { + type: 'string', + enum: ['global', 'project'], + description: + "Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.", }, }, required: [MEMORY_PARAM_FACT], diff --git a/packages/core/src/tools/memoryTool.test.ts b/packages/core/src/tools/memoryTool.test.ts index 4b0aa1b616..8b306c9fb6 100644 --- a/packages/core/src/tools/memoryTool.test.ts +++ b/packages/core/src/tools/memoryTool.test.ts @@ -19,7 +19,9 @@ import { getCurrentGeminiMdFilename, getAllGeminiMdFilenames, DEFAULT_CONTEXT_FILENAME, + getProjectMemoryFilePath, } from './memoryTool.js'; +import type { Storage } from '../config/storage.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -113,9 +115,7 @@ describe('MemoryTool', () => { it('should have correct name, displayName, description, and schema', () => { expect(memoryTool.name).toBe('save_memory'); expect(memoryTool.displayName).toBe('SaveMemory'); - expect(memoryTool.description).toContain( - 'Saves concise global user context', - ); + expect(memoryTool.description).toContain('Saves concise user context'); expect(memoryTool.schema).toBeDefined(); expect(memoryTool.schema.name).toBe('save_memory'); expect(memoryTool.schema.parametersJsonSchema).toStrictEqual({ @@ -127,6 +127,12 @@ describe('MemoryTool', () => { description: 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.', }, + scope: { + type: 'string', + enum: ['global', 'project'], + description: + "Where to save the memory. 'global' (default) saves to a file loaded in every workspace. 'project' saves to a project-specific file private to the user, not committed to the repo.", + }, }, required: ['fact'], }); @@ -378,4 +384,93 @@ describe('MemoryTool', () => { expect(() => memoryTool.build(attackParams)).toThrow(); }); }); + + describe('project-scope memory', () => { + const mockProjectMemoryDir = path.join( + '/mock', + '.gemini', + 'memory', + 'test-project', + ); + + function createMockStorage(): Storage { + return { + getProjectMemoryDir: () => mockProjectMemoryDir, + } as unknown as Storage; + } + + it('should reject scope=project when storage is not initialized', () => { + const bus = createMockMessageBus(); + const memoryToolNoStorage = new MemoryTool(bus); + const params = { fact: 'project fact', scope: 'project' as const }; + + expect(memoryToolNoStorage.validateToolParams(params)).toBe( + 'Project-level memory is not available: storage is not initialized.', + ); + }); + + it('should write to global path when scope is not specified', async () => { + const bus = createMockMessageBus(); + getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user'; + const memoryToolWithStorage = new MemoryTool(bus, createMockStorage()); + const params = { fact: 'global fact' }; + const invocation = memoryToolWithStorage.build(params); + await invocation.execute(mockAbortSignal); + + const expectedFilePath = path.join( + os.homedir(), + GEMINI_DIR, + getCurrentGeminiMdFilename(), + ); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedFilePath, + expect.any(String), + 'utf-8', + ); + }); + + it('should write to project memory path when scope is project', async () => { + const bus = createMockMessageBus(); + getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user'; + const memoryToolWithStorage = new MemoryTool(bus, createMockStorage()); + const params = { + fact: 'project-specific fact', + scope: 'project' as const, + }; + const invocation = memoryToolWithStorage.build(params); + await invocation.execute(mockAbortSignal); + + const expectedFilePath = path.join( + mockProjectMemoryDir, + getCurrentGeminiMdFilename(), + ); + expect(fs.mkdir).toHaveBeenCalledWith(mockProjectMemoryDir, { + recursive: true, + }); + expect(fs.writeFile).toHaveBeenCalledWith( + expectedFilePath, + expect.stringContaining('- project-specific fact'), + 'utf-8', + ); + }); + + it('should use project path in confirmation details when scope is project', async () => { + const bus = createMockMessageBus(); + getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user'; + const memoryToolWithStorage = new MemoryTool(bus, createMockStorage()); + const params = { fact: 'project fact', scope: 'project' as const }; + const invocation = memoryToolWithStorage.build(params); + const result = await invocation.shouldConfirmExecute(mockAbortSignal); + + expect(result).toBeDefined(); + expect(result).not.toBe(false); + + if (result && result.type === 'edit') { + expect(result.fileName).toBe( + getProjectMemoryFilePath(createMockStorage()), + ); + expect(result.newContent).toContain('- project fact'); + } + }); + }); }); diff --git a/packages/core/src/tools/memoryTool.ts b/packages/core/src/tools/memoryTool.ts index 68a0942a53..fa6a478d7d 100644 --- a/packages/core/src/tools/memoryTool.ts +++ b/packages/core/src/tools/memoryTool.ts @@ -61,6 +61,7 @@ export function getAllGeminiMdFilenames(): string[] { interface SaveMemoryParams { fact: string; + scope?: 'global' | 'project'; modified_by_user?: boolean; modified_content?: string; } @@ -69,6 +70,10 @@ export function getGlobalMemoryFilePath(): string { return path.join(Storage.getGlobalGeminiDir(), getCurrentGeminiMdFilename()); } +export function getProjectMemoryFilePath(storage: Storage): string { + return path.join(storage.getProjectMemoryDir(), getCurrentGeminiMdFilename()); +} + /** * Ensures proper newline separation before appending content. */ @@ -82,11 +87,11 @@ function ensureNewlineSeparation(currentContent: string): string { } /** - * Reads the current content of the memory file + * Reads the current content of a memory file at the given path. */ -async function readMemoryFileContent(): Promise { +async function readMemoryFileContent(filePath: string): Promise { try { - return await fs.readFile(getGlobalMemoryFilePath(), 'utf-8'); + return await fs.readFile(filePath, 'utf-8'); } catch (err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const error = err as Error & { code?: string }; @@ -146,32 +151,42 @@ class MemoryToolInvocation extends BaseToolInvocation< > { private static readonly allowlist: Set = new Set(); private proposedNewContent: string | undefined; + private readonly storage: Storage | undefined; constructor( params: SaveMemoryParams, messageBus: MessageBus, toolName?: string, displayName?: string, + storage?: Storage, ) { super(params, messageBus, toolName, displayName); + this.storage = storage; + } + + private getMemoryFilePath(): string { + if (this.params.scope === 'project' && this.storage) { + return getProjectMemoryFilePath(this.storage); + } + return getGlobalMemoryFilePath(); } getDescription(): string { - const memoryFilePath = getGlobalMemoryFilePath(); + const memoryFilePath = this.getMemoryFilePath(); return `in ${tildeifyPath(memoryFilePath)}`; } protected override async getConfirmationDetails( _abortSignal: AbortSignal, ): Promise { - const memoryFilePath = getGlobalMemoryFilePath(); + const memoryFilePath = this.getMemoryFilePath(); const allowlistKey = memoryFilePath; if (MemoryToolInvocation.allowlist.has(allowlistKey)) { return false; } - const currentContent = await readMemoryFileContent(); + const currentContent = await readMemoryFileContent(memoryFilePath); const { fact, modified_by_user, modified_content } = this.params; // If an attacker injects modified_content, use it for the diff @@ -213,6 +228,7 @@ class MemoryToolInvocation extends BaseToolInvocation< async execute(_signal: AbortSignal): Promise { const { fact, modified_by_user, modified_content } = this.params; + const memoryFilePath = this.getMemoryFilePath(); try { let contentToWrite: string; @@ -233,17 +249,17 @@ class MemoryToolInvocation extends BaseToolInvocation< // This case can be hit in flows without a confirmation step (e.g., --auto-confirm). // As a fallback, we recompute the content now. This is safe because // computeNewContent sanitizes the input. - const currentContent = await readMemoryFileContent(); + const currentContent = await readMemoryFileContent(memoryFilePath); this.proposedNewContent = computeNewContent(currentContent, fact); } contentToWrite = this.proposedNewContent; successMessage = `Okay, I've remembered that: "${sanitizedFact}"`; } - await fs.mkdir(path.dirname(getGlobalMemoryFilePath()), { + await fs.mkdir(path.dirname(memoryFilePath), { recursive: true, }); - await fs.writeFile(getGlobalMemoryFilePath(), contentToWrite, 'utf-8'); + await fs.writeFile(memoryFilePath, contentToWrite, 'utf-8'); return { llmContent: JSON.stringify({ @@ -275,8 +291,9 @@ export class MemoryTool implements ModifiableDeclarativeTool { static readonly Name = MEMORY_TOOL_NAME; + private readonly storage: Storage | undefined; - constructor(messageBus: MessageBus) { + constructor(messageBus: MessageBus, storage?: Storage) { super( MemoryTool.Name, 'SaveMemory', @@ -287,6 +304,14 @@ export class MemoryTool true, false, ); + this.storage = storage; + } + + private resolveMemoryFilePath(params: SaveMemoryParams): string { + if (params.scope === 'project' && this.storage) { + return getProjectMemoryFilePath(this.storage); + } + return getGlobalMemoryFilePath(); } protected override validateToolParamValues( @@ -296,6 +321,10 @@ export class MemoryTool return 'Parameter "fact" must be a non-empty string.'; } + if (params.scope === 'project' && !this.storage) { + return 'Project-level memory is not available: storage is not initialized.'; + } + return null; } @@ -310,6 +339,7 @@ export class MemoryTool messageBus, toolName ?? this.name, displayName ?? this.displayName, + this.storage, ); } @@ -319,11 +349,13 @@ export class MemoryTool getModifyContext(_abortSignal: AbortSignal): ModifyContext { return { - getFilePath: (_params: SaveMemoryParams) => getGlobalMemoryFilePath(), - getCurrentContent: async (_params: SaveMemoryParams): Promise => - readMemoryFileContent(), + getFilePath: (params: SaveMemoryParams) => + this.resolveMemoryFilePath(params), + getCurrentContent: async (params: SaveMemoryParams): Promise => + readMemoryFileContent(this.resolveMemoryFilePath(params)), getProposedContent: async (params: SaveMemoryParams): Promise => { - const currentContent = await readMemoryFileContent(); + const filePath = this.resolveMemoryFilePath(params); + const currentContent = await readMemoryFileContent(filePath); const { fact, modified_by_user, modified_content } = params; // Ensure the editor is populated with the same content // that the confirmation diff would show. diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index 01b9f9fb5a..cc61da78ec 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -485,6 +485,30 @@ export async function getGlobalMemoryPaths(): Promise { ); } +export async function getUserProjectMemoryPaths( + projectMemoryDir: string, +): Promise { + const geminiMdFilenames = getAllGeminiMdFilenames(); + + const accessChecks = geminiMdFilenames.map(async (filename) => { + const memoryPath = normalizePath(path.join(projectMemoryDir, filename)); + try { + await fs.access(memoryPath, fsSync.constants.R_OK); + debugLogger.debug( + '[DEBUG] [MemoryDiscovery] Found user project memory file:', + memoryPath, + ); + return memoryPath; + } catch { + return null; + } + }); + + return (await Promise.all(accessChecks)).filter( + (p): p is string => p !== null, + ); +} + export function getExtensionMemoryPaths( extensionLoader: ExtensionLoader, ): string[] { @@ -526,7 +550,12 @@ export async function getEnvironmentMemoryPaths( } export function categorizeAndConcatenate( - paths: { global: string[]; extension: string[]; project: string[] }, + paths: { + global: string[]; + extension: string[]; + project: string[]; + userProjectMemory?: string[]; + }, contentsMap: Map, ): HierarchicalMemory { const getConcatenated = (pList: string[]) => @@ -540,6 +569,7 @@ export function categorizeAndConcatenate( global: getConcatenated(paths.global), extension: getConcatenated(paths.extension), project: getConcatenated(paths.project), + userProjectMemory: getConcatenated(paths.userProjectMemory ?? []), }; }