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
@@ -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,
+23 -4
View File
@@ -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<string, GeminiFileContent>,
) {
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));
}