feat(core): add project-level memory scope to save_memory tool (#24161)

This commit is contained in:
Sandy Tao
2026-03-30 18:32:15 -07:00
committed by GitHub
parent d5733f7b71
commit 0589daf06f
19 changed files with 382 additions and 63 deletions
+3
View File
@@ -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
+13 -5
View File
@@ -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, () =>
+7
View File
@@ -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() });
}
+11
View File
@@ -147,6 +147,17 @@ describe('Storage additional helpers', () => {
expect(storage.getProjectAgentsDir()).toBe(expected);
});
it('getProjectMemoryDir returns ~/.gemini/memory/<identifier>', 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(),
+5
View File
@@ -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');
}