chore: clean up launched memory features

This commit is contained in:
Sandy Tao
2026-05-12 13:06:07 -07:00
parent ebe15553a9
commit e431f6e479
82 changed files with 604 additions and 4166 deletions
@@ -90,6 +90,7 @@ describe('getEnvironmentContext', () => {
getFileService: vi.fn(),
getIncludeDirectoryTree: vi.fn().mockReturnValue(true),
getEnvironmentMemory: vi.fn().mockReturnValue('Mock Environment Memory'),
getSessionMemory: vi.fn().mockReturnValue('Mock Session Memory'),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
storage: {
@@ -116,7 +117,7 @@ describe('getEnvironmentContext', () => {
expect(context).toContain(
'- **Directory Structure:**\n\nMock Folder Structure',
);
expect(context).toContain('Mock Environment Memory');
expect(context).toContain('Mock Session Memory');
expect(context).toContain('</session_context>');
expect(getFolderStructure).toHaveBeenCalledWith('/test/dir', {
fileService: undefined,
@@ -160,15 +161,12 @@ describe('getEnvironmentContext', () => {
expect(context).toContain('<session_context>');
expect(context).not.toContain('Directory Structure:');
expect(context).not.toContain('Mock Folder Structure');
expect(context).toContain('Mock Environment Memory');
expect(context).toContain('Mock Session Memory');
expect(context).toContain('</session_context>');
expect(getFolderStructure).not.toHaveBeenCalled();
});
it('should use session memory instead of environment memory when JIT context is enabled', async () => {
(mockConfig as Record<string, unknown>)['isJitContextEnabled'] = vi
.fn()
.mockReturnValue(true);
it('should use session memory instead of environment memory', async () => {
(mockConfig as Record<string, unknown>)['getSessionMemory'] = vi
.fn()
.mockReturnValue(
@@ -188,17 +186,6 @@ describe('getEnvironmentContext', () => {
expect(context).toContain('</loaded_context>');
});
it('should include environment memory when JIT context is disabled', async () => {
(mockConfig as Record<string, unknown>)['isJitContextEnabled'] = vi
.fn()
.mockReturnValue(false);
const parts = await getEnvironmentContext(mockConfig as Config);
const context = parts[0].text;
expect(context).toContain('Mock Environment Memory');
});
it('should handle read_many_files returning no content', async () => {
const mockReadManyFilesTool = {
build: vi.fn().mockReturnValue({
@@ -61,12 +61,7 @@ export async function getEnvironmentContext(config: Config): Promise<Part[]> {
// - Tier 1 (global): system instruction only
// - Tier 2 (extension + project): first user message (here)
// - Tier 3 (subdirectory): tool output (JIT)
// When JIT is enabled, Tier 2 memory is provided by getSessionMemory().
// When JIT is disabled, all memory is in the system instruction and
// getEnvironmentMemory() provides the project memory for this message.
const environmentMemory = config.isJitContextEnabled?.()
? config.getSessionMemory()
: config.getEnvironmentMemory();
const environmentMemory = config.getSessionMemory();
const context = `
<session_context>
+32 -17
View File
@@ -19,16 +19,6 @@ import type { Config, GeminiCLIExtension } from '../config/config.js';
import { type McpClientManager } from '../tools/mcp-client-manager.js';
import type { GeminiClient } from '../core/client.js';
const mockRefreshServerHierarchicalMemory = vi.hoisted(() => vi.fn());
vi.mock('./memoryDiscovery.js', async (importActual) => {
const actual = await importActual<typeof import('./memoryDiscovery.js')>();
return {
...actual,
refreshServerHierarchicalMemory: mockRefreshServerHierarchicalMemory,
};
});
describe('SimpleExtensionLoader', () => {
let mockConfig: Config;
let extensionReloadingEnabled: boolean;
@@ -36,6 +26,8 @@ describe('SimpleExtensionLoader', () => {
let mockGeminiClientSetTools: MockInstance<
typeof GeminiClient.prototype.setTools
>;
let mockGeminiClientUpdateSystemInstruction: MockInstance;
let mockMemoryRefresh: MockInstance;
let mockHookSystemInit: MockInstance;
let mockAgentRegistryReload: MockInstance;
let mockSkillsReload: MockInstance;
@@ -86,6 +78,8 @@ describe('SimpleExtensionLoader', () => {
} as unknown as McpClientManager;
extensionReloadingEnabled = false;
mockGeminiClientSetTools = vi.fn();
mockGeminiClientUpdateSystemInstruction = vi.fn();
mockMemoryRefresh = vi.fn();
mockHookSystemInit = vi.fn();
mockAgentRegistryReload = vi.fn();
mockSkillsReload = vi.fn();
@@ -101,10 +95,15 @@ describe('SimpleExtensionLoader', () => {
geminiClient: {
isInitialized: () => true,
setTools: mockGeminiClientSetTools,
updateSystemInstruction: mockGeminiClientUpdateSystemInstruction,
},
getGeminiClient: vi.fn(() => ({
isInitialized: () => true,
setTools: mockGeminiClientSetTools,
updateSystemInstruction: mockGeminiClientUpdateSystemInstruction,
})),
getMemoryContextManager: vi.fn(() => ({
refresh: mockMemoryRefresh,
})),
getHookSystem: () => ({
initialize: mockHookSystemInit,
@@ -193,20 +192,27 @@ describe('SimpleExtensionLoader', () => {
expect(
mockMcpClientManager.startExtension,
).toHaveBeenCalledExactlyOnceWith(activeExtension);
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockMemoryRefresh).toHaveBeenCalledOnce();
expect(
mockGeminiClientUpdateSystemInstruction,
).toHaveBeenCalledOnce();
expect(mockHookSystemInit).toHaveBeenCalledOnce();
expect(mockGeminiClientSetTools).toHaveBeenCalledOnce();
expect(mockAgentRegistryReload).toHaveBeenCalledOnce();
expect(mockSkillsReload).toHaveBeenCalledOnce();
} else {
expect(mockMcpClientManager.startExtension).not.toHaveBeenCalled();
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
expect(mockMemoryRefresh).not.toHaveBeenCalled();
expect(
mockGeminiClientUpdateSystemInstruction,
).not.toHaveBeenCalled();
expect(mockHookSystemInit).not.toHaveBeenCalled();
expect(mockGeminiClientSetTools).not.toHaveBeenCalledOnce();
expect(mockAgentRegistryReload).not.toHaveBeenCalled();
expect(mockSkillsReload).not.toHaveBeenCalled();
}
mockRefreshServerHierarchicalMemory.mockClear();
mockMemoryRefresh.mockClear();
mockGeminiClientUpdateSystemInstruction.mockClear();
mockHookSystemInit.mockClear();
mockGeminiClientSetTools.mockClear();
mockAgentRegistryReload.mockClear();
@@ -217,14 +223,20 @@ describe('SimpleExtensionLoader', () => {
expect(
mockMcpClientManager.stopExtension,
).toHaveBeenCalledExactlyOnceWith(activeExtension);
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockMemoryRefresh).toHaveBeenCalledOnce();
expect(
mockGeminiClientUpdateSystemInstruction,
).toHaveBeenCalledOnce();
expect(mockHookSystemInit).toHaveBeenCalledOnce();
expect(mockGeminiClientSetTools).toHaveBeenCalledOnce();
expect(mockAgentRegistryReload).toHaveBeenCalledOnce();
expect(mockSkillsReload).toHaveBeenCalledOnce();
} else {
expect(mockMcpClientManager.stopExtension).not.toHaveBeenCalled();
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
expect(mockMemoryRefresh).not.toHaveBeenCalled();
expect(
mockGeminiClientUpdateSystemInstruction,
).not.toHaveBeenCalled();
expect(mockHookSystemInit).not.toHaveBeenCalled();
expect(mockGeminiClientSetTools).not.toHaveBeenCalledOnce();
expect(mockAgentRegistryReload).not.toHaveBeenCalled();
@@ -242,12 +254,15 @@ describe('SimpleExtensionLoader', () => {
const loader = new SimpleExtensionLoader([]);
await loader.loadExtension(activeExtension);
await loader.start(mockConfig);
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
expect(mockMemoryRefresh).not.toHaveBeenCalled();
await Promise.all([
loader.unloadExtension(activeExtension),
loader.loadExtension(anotherExtension),
]);
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockMemoryRefresh).toHaveBeenCalledOnce();
expect(
mockGeminiClientUpdateSystemInstruction,
).toHaveBeenCalledOnce();
expect(mockHookSystemInit).toHaveBeenCalledOnce();
expect(mockAgentRegistryReload).toHaveBeenCalledOnce();
expect(mockSkillsReload).toHaveBeenCalledOnce();
+2 -2
View File
@@ -6,7 +6,6 @@
import type { EventEmitter } from 'node:events';
import type { Config, GeminiCLIExtension } from '../config/config.js';
import { refreshServerHierarchicalMemory } from './memoryDiscovery.js';
export abstract class ExtensionLoader {
// Assigned in `start`.
@@ -125,7 +124,8 @@ export abstract class ExtensionLoader {
// Wait until all extensions are done starting and stopping before we
// reload memory, this is somewhat expensive and also busts the context
// cache, we want to only do it once.
await refreshServerHierarchicalMemory(this.config);
await this.config.getMemoryContextManager()?.refresh();
this.config.getGeminiClient().updateSystemInstruction();
await this.config.getHookSystem()?.initialize();
await this.config.getAgentRegistry().reload();
await this.config.reloadSkills();
+37 -827
View File
@@ -9,13 +9,12 @@ import * as fsPromises from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
loadServerHierarchicalMemory,
deduplicatePathsByFileIdentity,
getGlobalMemoryPaths,
getExtensionMemoryPaths,
getEnvironmentMemoryPaths,
getUserProjectMemoryPaths,
loadJitSubdirectoryMemory,
refreshServerHierarchicalMemory,
readGeminiMdFiles,
} from './memoryDiscovery.js';
import {
@@ -23,29 +22,13 @@ import {
DEFAULT_CONTEXT_FILENAME,
PROJECT_MEMORY_INDEX_FILENAME,
} from '../tools/memoryTool.js';
import { flattenMemory, type HierarchicalMemory } from '../config/memory.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import {
GEMINI_DIR,
toAbsolutePath,
homedir as pathsHomedir,
} from './paths.js';
function flattenResult(result: {
memoryContent: HierarchicalMemory;
fileCount: number;
filePaths: string[];
}) {
return {
...result,
memoryContent: flattenMemory(result.memoryContent),
filePaths: result.filePaths,
};
}
import { Config, type GeminiCLIExtension } from '../config/config.js';
import { Storage } from '../config/storage.js';
import type { GeminiCLIExtension } from '../config/config.js';
import { SimpleExtensionLoader } from './extensionLoader.js';
import { CoreEvent, coreEvents } from './events.js';
vi.mock('os', async (importOriginal) => {
const actualOs = await importOriginal<typeof os>();
@@ -68,9 +51,7 @@ vi.mock('../utils/paths.js', async (importOriginal) => {
});
describe('memoryDiscovery', () => {
const DEFAULT_FOLDER_TRUST = true;
let testRootDir: string;
let cwd: string;
let projectRoot: string;
let homedir: string;
@@ -98,7 +79,6 @@ describe('memoryDiscovery', () => {
vi.stubEnv('VITEST', 'true');
projectRoot = await createEmptyDir(path.join(testRootDir, 'project'));
cwd = await createEmptyDir(path.join(projectRoot, 'src'));
homedir = await createEmptyDir(path.join(testRootDir, 'userhome'));
vi.mocked(os.homedir).mockReturnValue(homedir);
vi.mocked(pathsHomedir).mockReturnValue(homedir);
@@ -118,578 +98,10 @@ describe('memoryDiscovery', () => {
});
});
describe('when untrusted', () => {
it('does not load context files from untrusted workspaces', async () => {
await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory',
);
await createTestFile(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
'Src directory memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
false, // untrusted
),
);
expect(result).toEqual({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
});
it('loads context from outside the untrusted workspace', async () => {
await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory', // Untrusted
);
await createTestFile(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
'Src directory memory', // Untrusted
);
const filepathInput = path.join(
homedir,
GEMINI_DIR,
DEFAULT_CONTEXT_FILENAME,
);
const filepath = await createTestFile(
filepathInput,
'default context content',
); // In user home dir (outside untrusted space).
const { fileCount, memoryContent, filePaths } = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
false, // untrusted
),
);
expect(fileCount).toEqual(1);
expect(memoryContent).toContain(filepath);
expect(filePaths).toEqual([filepath]);
});
});
it('should return empty memory and count if no context files are found', async () => {
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
});
it('should load only the global context file if present and others are not (default filename)', async () => {
const defaultContextFile = await createTestFile(
path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME),
'default context content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect({
...result,
memoryContent: flattenMemory(result.memoryContent),
}).toEqual({
memoryContent: `--- Global ---
--- Context from: ${defaultContextFile} ---
default context content
--- End of Context from: ${defaultContextFile} ---`,
fileCount: 1,
filePaths: [defaultContextFile],
});
});
it('should load only the global custom context file if present and filename is changed', async () => {
const customFilename = 'CUSTOM_AGENTS.md';
setGeminiMdFilename(customFilename);
const customContextFile = await createTestFile(
path.join(homedir, GEMINI_DIR, customFilename),
'custom context content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Global ---
--- Context from: ${customContextFile} ---
custom context content
--- End of Context from: ${customContextFile} ---`,
fileCount: 1,
filePaths: [customContextFile],
});
});
it('should load context files by upward traversal with custom filename', async () => {
const customFilename = 'PROJECT_CONTEXT.md';
setGeminiMdFilename(customFilename);
const projectContextFile = await createTestFile(
path.join(projectRoot, customFilename),
'project context content',
);
const cwdContextFile = await createTestFile(
path.join(cwd, customFilename),
'cwd context content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${projectContextFile} ---
project context content
--- End of Context from: ${projectContextFile} ---
--- Context from: ${cwdContextFile} ---
cwd context content
--- End of Context from: ${cwdContextFile} ---`,
fileCount: 2,
filePaths: [projectContextFile, cwdContextFile],
});
});
it('should load context files by downward traversal with custom filename', async () => {
const customFilename = 'LOCAL_CONTEXT.md';
setGeminiMdFilename(customFilename);
const subdirCustomFile = await createTestFile(
path.join(cwd, 'subdir', customFilename),
'Subdir custom memory',
);
const cwdCustomFile = await createTestFile(
path.join(cwd, customFilename),
'CWD custom memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${cwdCustomFile} ---
CWD custom memory
--- End of Context from: ${cwdCustomFile} ---
--- Context from: ${subdirCustomFile} ---
Subdir custom memory
--- End of Context from: ${subdirCustomFile} ---`,
fileCount: 2,
filePaths: [cwdCustomFile, subdirCustomFile],
});
});
it('should load ORIGINAL_GEMINI_MD_FILENAME files by upward traversal from CWD to project root', async () => {
const projectRootGeminiFile = await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory',
);
const srcGeminiFile = await createTestFile(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
'Src directory memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${projectRootGeminiFile} ---
Project root memory
--- End of Context from: ${projectRootGeminiFile} ---
--- Context from: ${srcGeminiFile} ---
Src directory memory
--- End of Context from: ${srcGeminiFile} ---`,
fileCount: 2,
filePaths: [projectRootGeminiFile, srcGeminiFile],
});
});
it('should load ORIGINAL_GEMINI_MD_FILENAME files by downward traversal from CWD', async () => {
const subDirGeminiFile = await createTestFile(
path.join(cwd, 'subdir', DEFAULT_CONTEXT_FILENAME),
'Subdir memory',
);
const cwdGeminiFile = await createTestFile(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
'CWD memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${cwdGeminiFile} ---
CWD memory
--- End of Context from: ${cwdGeminiFile} ---
--- Context from: ${subDirGeminiFile} ---
Subdir memory
--- End of Context from: ${subDirGeminiFile} ---`,
fileCount: 2,
filePaths: [cwdGeminiFile, subDirGeminiFile],
});
});
it('should load and correctly order global, upward, and downward ORIGINAL_GEMINI_MD_FILENAME files', async () => {
const defaultContextFile = await createTestFile(
path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME),
'default context content',
);
const rootGeminiFile = await createTestFile(
path.join(testRootDir, DEFAULT_CONTEXT_FILENAME),
'Project parent memory',
);
const projectRootGeminiFile = await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory',
);
const cwdGeminiFile = await createTestFile(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
'CWD memory',
);
const subDirGeminiFile = await createTestFile(
path.join(cwd, 'sub', DEFAULT_CONTEXT_FILENAME),
'Subdir memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Global ---
--- Context from: ${defaultContextFile} ---
default context content
--- End of Context from: ${defaultContextFile} ---
--- Project ---
--- Context from: ${rootGeminiFile} ---
Project parent memory
--- End of Context from: ${rootGeminiFile} ---
--- Context from: ${projectRootGeminiFile} ---
Project root memory
--- End of Context from: ${projectRootGeminiFile} ---
--- Context from: ${cwdGeminiFile} ---
CWD memory
--- End of Context from: ${cwdGeminiFile} ---
--- Context from: ${subDirGeminiFile} ---
Subdir memory
--- End of Context from: ${subDirGeminiFile} ---`,
fileCount: 5,
filePaths: [
defaultContextFile,
rootGeminiFile,
projectRootGeminiFile,
cwdGeminiFile,
subDirGeminiFile,
],
});
});
it('should ignore specified directories during downward scan', async () => {
await createEmptyDir(path.join(projectRoot, '.git'));
await createTestFile(path.join(projectRoot, '.gitignore'), 'node_modules');
await createTestFile(
path.join(cwd, 'node_modules', DEFAULT_CONTEXT_FILENAME),
'Ignored memory',
);
const regularSubDirGeminiFile = await createTestFile(
path.join(cwd, 'my_code', DEFAULT_CONTEXT_FILENAME),
'My code memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
'tree',
{
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
200, // maxDirs parameter
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${regularSubDirGeminiFile} ---
My code memory
--- End of Context from: ${regularSubDirGeminiFile} ---`,
fileCount: 1,
filePaths: [regularSubDirGeminiFile],
});
});
it('should respect the maxDirs parameter during downward scan', async () => {
// Create directories in parallel for better performance
const dirPromises = Array.from({ length: 2 }, (_, i) =>
createEmptyDir(path.join(cwd, `deep_dir_${i}`)),
);
await Promise.all(dirPromises);
// Pass the custom limit directly to the function
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
'tree', // importFormat
{
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
1, // maxDirs
);
// Note: bfsFileSearch debug logging is no longer controlled via debugMode parameter
// The test verifies maxDirs is respected by checking the result, not debug logs
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
});
it('should load extension context file paths', async () => {
const extensionFilePath = await createTestFile(
path.join(testRootDir, 'extensions/ext1/GEMINI.md'),
'Extension memory content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([
{
contextFiles: [extensionFilePath],
isActive: true,
} as GeminiCLIExtension,
]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Extension ---
--- Context from: ${extensionFilePath} ---
Extension memory content
--- End of Context from: ${extensionFilePath} ---`,
fileCount: 1,
filePaths: [extensionFilePath],
});
});
it('should load memory from included directories', async () => {
const includedDir = await createEmptyDir(
path.join(testRootDir, 'included'),
);
const includedFile = await createTestFile(
path.join(includedDir, DEFAULT_CONTEXT_FILENAME),
'included directory memory',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[includedDir],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result).toEqual({
memoryContent: `--- Project ---
--- Context from: ${includedFile} ---
included directory memory
--- End of Context from: ${includedFile} ---`,
fileCount: 1,
filePaths: [includedFile],
});
});
it('should handle multiple directories and files in parallel correctly', async () => {
// Create multiple test directories with GEMINI.md files
const numDirs = 5;
const createdFiles: string[] = [];
for (let i = 0; i < numDirs; i++) {
const dirPath = await createEmptyDir(
path.join(testRootDir, `project-${i}`),
);
const filePath = await createTestFile(
path.join(dirPath, DEFAULT_CONTEXT_FILENAME),
`Content from project ${i}`,
);
createdFiles.push(filePath);
}
// Load memory from all directories
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
createdFiles.map((f) => path.dirname(f)),
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// Should have loaded all files
expect(result.fileCount).toBe(numDirs);
expect(result.filePaths.length).toBe(numDirs);
expect(result.filePaths.sort()).toEqual(createdFiles.sort());
// Content should include all project contents
const flattenedMemory = flattenMemory(result.memoryContent);
for (let i = 0; i < numDirs; i++) {
expect(flattenedMemory).toContain(`Content from project ${i}`);
}
});
it('should preserve order and prevent duplicates when processing multiple directories', async () => {
// Create overlapping directory structure
const parentDir = await createEmptyDir(path.join(testRootDir, 'parent'));
const childDir = await createEmptyDir(path.join(parentDir, 'child'));
const parentFile = await createTestFile(
path.join(parentDir, DEFAULT_CONTEXT_FILENAME),
'Parent content',
);
const childFile = await createTestFile(
path.join(childDir, DEFAULT_CONTEXT_FILENAME),
'Child content',
);
// Include both parent and child directories
const result = flattenResult(
await loadServerHierarchicalMemory(
parentDir,
[childDir, parentDir], // Deliberately include duplicates
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// Should have both files without duplicates
const flattenedMemory = flattenMemory(result.memoryContent);
expect(result.fileCount).toBe(2);
expect(flattenedMemory).toContain('Parent content');
expect(flattenedMemory).toContain('Child content');
expect(result.filePaths.sort()).toEqual([parentFile, childFile].sort());
// Check that files are not duplicated
const parentOccurrences = (flattenedMemory.match(/Parent content/g) || [])
.length;
const childOccurrences = (flattenedMemory.match(/Child content/g) || [])
.length;
expect(parentOccurrences).toBe(1);
expect(childOccurrences).toBe(1);
});
describe('EISDIR handling for GEMINI.md as a directory', () => {
it('readGeminiMdFiles returns null content (without throwing) when path is a directory', async () => {
const dirAsFilePath = await createEmptyDir(
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
);
const results = await readGeminiMdFiles([dirAsFilePath]);
@@ -698,83 +110,6 @@ included directory memory
expect(results[0].filePath).toBe(dirAsFilePath);
expect(results[0].content).toBeNull();
});
it('loadServerHierarchicalMemory ignores a GEMINI.md directory and returns empty memory', async () => {
// Create a directory named GEMINI.md where a regular file would be expected.
await createEmptyDir(path.join(cwd, DEFAULT_CONTEXT_FILENAME));
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// EISDIR is silently skipped, so memory is empty (no readable file
// contents) and no exception propagates.
expect(result.memoryContent).toBe('');
});
it('falls back to a real GEMINI.md file at a higher level when a directory shadows the same name lower in the tree', async () => {
// Lower in the tree (cwd): a directory named GEMINI.md (invalid).
await createEmptyDir(path.join(cwd, DEFAULT_CONTEXT_FILENAME));
// Higher in the tree (projectRoot): a real GEMINI.md file (valid).
const projectContextFile = await createTestFile(
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
'Project root memory content',
);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// The directory at cwd is silently skipped; the actual file at
// projectRoot is still discovered and loaded normally.
expect(result.memoryContent).toContain('Project root memory content');
expect(result.filePaths).toContain(projectContextFile);
});
it('silently skips a GEMINI.md symlink that points to a directory', async () => {
// Create a real directory elsewhere and symlink GEMINI.md to it.
const realDir = await createEmptyDir(path.join(cwd, '.geminimd-target'));
const symlinkPath = path.join(cwd, DEFAULT_CONTEXT_FILENAME);
try {
await fsPromises.symlink(realDir, symlinkPath, 'dir');
} catch (err) {
// Symlink creation may be unsupported on some Windows setups (no
// SeCreateSymbolicLinkPrivilege). Skip the test there rather than fail.
if (
err instanceof Error &&
(err as NodeJS.ErrnoException).code === 'EPERM'
) {
return;
}
throw err;
}
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
// A symlink resolving to a directory triggers EISDIR on read in the
// same way a plain directory does and must be skipped silently.
expect(result.memoryContent).toBe('');
});
});
describe('getGlobalMemoryPaths', () => {
@@ -1061,7 +396,7 @@ included directory memory
});
});
describe('case-insensitive filesystem deduplication', () => {
describe('file identity deduplication', () => {
it('should deduplicate files that point to the same inode (same physical file)', async () => {
const geminiFile = await createTestFile(
path.join(projectRoot, 'gemini.md'),
@@ -1090,24 +425,16 @@ included directory memory
expect(stats1.ino).toBe(stats2.ino);
expect(stats1.dev).toBe(stats2.dev);
setGeminiMdFilename(['GEMINI.md', 'gemini.md']);
const result = await deduplicatePathsByFileIdentity([
geminiFileLink,
geminiFile,
]);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
expect(result.paths).toHaveLength(1);
expect(result.identityMap.get(geminiFile)).toBe(
result.identityMap.get(geminiFileLink),
);
expect(result.fileCount).toBe(1);
expect(result.filePaths).toHaveLength(1);
expect(result.memoryContent).toContain('Project root memory');
const contentMatches = result.memoryContent.match(/Project root memory/g);
expect(contentMatches).toHaveLength(1);
try {
await fsPromises.unlink(geminiFileLink);
} catch {
@@ -1129,45 +456,31 @@ included directory memory
const stats2 = await fsPromises.lstat(geminiFileUpper);
if (stats1.ino !== stats2.ino || stats1.dev !== stats2.dev) {
setGeminiMdFilename(['GEMINI.md', 'gemini.md']);
const result = await deduplicatePathsByFileIdentity([
geminiFileLower,
geminiFileUpper,
]);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result.fileCount).toBe(2);
expect(result.filePaths).toHaveLength(2);
expect(result.memoryContent).toContain('Lowercase file content');
expect(result.memoryContent).toContain('Uppercase file content');
expect(result.paths).toHaveLength(2);
expect(result.paths).toContain(geminiFileLower);
expect(result.paths).toContain(geminiFileUpper);
}
});
it("should handle files that cannot be stat'd (missing files)", async () => {
await createTestFile(
const geminiFile = await createTestFile(
path.join(projectRoot, 'gemini.md'),
'Valid file content',
);
const missingFile = path.join(projectRoot, 'missing.md');
setGeminiMdFilename(['gemini.md', 'missing.md']);
const result = await deduplicatePathsByFileIdentity([
geminiFile,
missingFile,
]);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
);
expect(result.fileCount).toBe(1);
expect(result.memoryContent).toContain('Valid file content');
expect(result.paths).toEqual([geminiFile, missingFile]);
expect(result.identityMap.has(missingFile)).toBe(false);
});
it('should deduplicate multiple paths pointing to same file (3+ duplicates)', async () => {
@@ -1201,23 +514,19 @@ included directory memory
expect(stats1.ino).toBe(stats2.ino);
expect(stats1.ino).toBe(stats3.ino);
setGeminiMdFilename(['gemini.md', 'GEMINI.md', 'Gemini.md']);
const result = await deduplicatePathsByFileIdentity([
geminiFile,
link1,
link2,
]);
const result = flattenResult(
await loadServerHierarchicalMemory(
cwd,
[],
new FileDiscoveryService(projectRoot),
new SimpleExtensionLoader([]),
DEFAULT_FOLDER_TRUST,
),
expect(result.paths).toHaveLength(1);
expect(result.identityMap.get(geminiFile)).toBe(
result.identityMap.get(link1),
);
expect(result.identityMap.get(geminiFile)).toBe(
result.identityMap.get(link2),
);
expect(result.fileCount).toBe(1);
expect(result.filePaths).toHaveLength(1);
expect(result.memoryContent).toContain('Project root memory');
const contentMatches = result.memoryContent.match(/Project root memory/g);
expect(contentMatches).toHaveLength(1);
try {
await fsPromises.unlink(link1);
@@ -1554,103 +863,4 @@ included directory memory
expect(result.files.find((f) => f.path === subDirMemory)).toBeDefined();
});
});
it('refreshServerHierarchicalMemory should refresh memory and update config', async () => {
const extensionLoader = new SimpleExtensionLoader([]);
const config = new Config({
sessionId: '1',
targetDir: cwd,
cwd,
debugMode: false,
model: 'fake-model',
extensionLoader,
});
const result = flattenResult(
await loadServerHierarchicalMemory(
config.getWorkingDir(),
config.shouldLoadMemoryFromIncludeDirectories()
? config.getWorkspaceContext().getDirectories()
: [],
config.getFileService(),
config.getExtensionLoader(),
config.isTrustedFolder(),
config.getImportFormat(),
),
);
expect(result.fileCount).equals(0);
// Now add an extension with a memory file
const extensionsDir = new Storage(homedir).getExtensionsDir();
const extensionPath = path.join(extensionsDir, 'new-extension');
const contextFilePath = path.join(extensionPath, 'CustomContext.md');
await fsPromises.mkdir(extensionPath, { recursive: true });
await fsPromises.writeFile(contextFilePath, 'Really cool custom context!');
await extensionLoader.loadExtension({
name: 'new-extension',
isActive: true,
contextFiles: [contextFilePath],
version: '1.0.0',
id: '1234',
path: extensionPath,
});
const mockEventListener = vi.fn();
coreEvents.on(CoreEvent.MemoryChanged, mockEventListener);
const refreshResult = await refreshServerHierarchicalMemory(config);
expect(refreshResult.fileCount).equals(1);
expect(config.getGeminiMdFileCount()).equals(refreshResult.fileCount);
const flattenedMemory = flattenMemory(refreshResult.memoryContent);
expect(flattenedMemory).toContain('Really cool custom context!');
expect(config.getUserMemory()).toStrictEqual(refreshResult.memoryContent);
expect(refreshResult.filePaths[0]).toContain(
toAbsolutePath(path.join(extensionPath, 'CustomContext.md')),
);
expect(config.getGeminiMdFilePaths()).equals(refreshResult.filePaths);
expect(mockEventListener).toHaveBeenCalledExactlyOnceWith({
fileCount: refreshResult.fileCount,
});
});
it('should include MCP instructions in user memory', async () => {
const mockConfig = {
getWorkingDir: vi.fn().mockReturnValue(cwd),
shouldLoadMemoryFromIncludeDirectories: vi.fn().mockReturnValue(false),
getFileService: vi
.fn()
.mockReturnValue(new FileDiscoveryService(projectRoot)),
getExtensionLoader: vi
.fn()
.mockReturnValue(new SimpleExtensionLoader([])),
isTrustedFolder: vi.fn().mockReturnValue(true),
getImportFormat: vi.fn().mockReturnValue('tree'),
getFileFilteringOptions: vi.fn().mockReturnValue(undefined),
getDiscoveryMaxDirs: vi.fn().mockReturnValue(200),
getMemoryBoundaryMarkers: vi.fn().mockReturnValue(['.git']),
setUserMemory: vi.fn(),
setGeminiMdFileCount: vi.fn(),
setGeminiMdFilePaths: vi.fn(),
getMcpClientManager: vi.fn().mockReturnValue({
getMcpInstructions: vi
.fn()
.mockReturnValue(
"\n\n# Instructions for MCP Server 'extension-server'\nAlways be polite.",
),
}),
} as unknown as Config;
await refreshServerHierarchicalMemory(mockConfig);
expect(mockConfig.setUserMemory).toHaveBeenCalledWith(
expect.objectContaining({
project: expect.stringContaining(
"# Instructions for MCP Server 'extension-server'",
),
}),
);
expect(mockConfig.setUserMemory).toHaveBeenCalledWith(
expect.objectContaining({
project: expect.stringContaining('Always be polite.'),
}),
);
});
});
-323
View File
@@ -7,17 +7,11 @@
import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs';
import * as path from 'node:path';
import { bfsFileSearch } from './bfsFileSearch.js';
import {
getAllGeminiMdFilenames,
PROJECT_MEMORY_INDEX_FILENAME,
} from '../tools/memoryTool.js';
import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { processImports } from './memoryImportProcessor.js';
import {
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
type FileFilteringOptions,
} from '../config/constants.js';
import {
GEMINI_DIR,
homedir,
@@ -27,9 +21,7 @@ import {
} from './paths.js';
import type { ExtensionLoader } from './extensionLoader.js';
import { debugLogger } from './debugLogger.js';
import type { Config } from '../config/config.js';
import type { HierarchicalMemory } from '../config/memory.js';
import { CoreEvent, coreEvents } from './events.js';
import { getErrorMessage } from './errors.js';
// Simple console logger, similar to the one previously in CLI's config.ts
@@ -214,169 +206,6 @@ async function findProjectRoot(
}
}
async function getGeminiMdFilePathsInternal(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[],
userHomePath: string,
fileService: FileDiscoveryService,
folderTrust: boolean,
fileFilteringOptions: FileFilteringOptions,
maxDirs: number,
boundaryMarkers: readonly string[] = ['.git'],
): Promise<{ global: string[]; project: string[] }> {
const dirs = new Set<string>([
...includeDirectoriesToReadGemini,
currentWorkingDirectory,
]);
// Process directories in parallel with concurrency limit to prevent EMFILE errors
const CONCURRENT_LIMIT = 10;
const dirsArray = Array.from(dirs);
const globalPaths = new Set<string>();
const projectPaths = new Set<string>();
for (let i = 0; i < dirsArray.length; i += CONCURRENT_LIMIT) {
const batch = dirsArray.slice(i, i + CONCURRENT_LIMIT);
const batchPromises = batch.map((dir) =>
getGeminiMdFilePathsInternalForEachDir(
dir,
userHomePath,
fileService,
folderTrust,
fileFilteringOptions,
maxDirs,
boundaryMarkers,
),
);
const batchResults = await Promise.allSettled(batchPromises);
for (const result of batchResults) {
if (result.status === 'fulfilled') {
result.value.global.forEach((p) => globalPaths.add(p));
result.value.project.forEach((p) => projectPaths.add(p));
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const error = result.reason;
const message = error instanceof Error ? error.message : String(error);
logger.error(`Error discovering files in directory: ${message}`);
}
}
}
return {
global: Array.from(globalPaths),
project: Array.from(projectPaths),
};
}
async function getGeminiMdFilePathsInternalForEachDir(
dir: string,
userHomePath: string,
fileService: FileDiscoveryService,
folderTrust: boolean,
fileFilteringOptions: FileFilteringOptions,
maxDirs: number,
boundaryMarkers: readonly string[] = ['.git'],
): Promise<{ global: string[]; project: string[] }> {
const globalPaths = new Set<string>();
const projectPaths = new Set<string>();
const geminiMdFilenames = getAllGeminiMdFilenames();
for (const geminiMdFilename of geminiMdFilenames) {
const resolvedHome = toAbsolutePath(userHomePath);
const globalGeminiDir = toAbsolutePath(path.join(resolvedHome, GEMINI_DIR));
const globalMemoryPath = toAbsolutePath(
path.join(globalGeminiDir, geminiMdFilename),
);
const globalMemoryKey = normalizePath(globalMemoryPath);
const globalGeminiDirKey = normalizePath(globalGeminiDir);
// This part that finds the global file always runs.
try {
await fs.access(globalMemoryPath, fsSync.constants.R_OK);
globalPaths.add(globalMemoryPath);
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Found readable global',
geminiMdFilename + ':',
globalMemoryPath,
);
} catch {
// It's okay if it's not found.
}
// FIX: Only perform the workspace search (upward and downward scans)
// if a valid currentWorkingDirectory is provided.
if (dir && folderTrust) {
const resolvedCwd = toAbsolutePath(dir);
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Searching for',
geminiMdFilename,
'starting from CWD:',
resolvedCwd,
);
const projectRoot = await findProjectRoot(resolvedCwd, boundaryMarkers);
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Determined project root:',
projectRoot ?? 'None',
);
const upwardPaths: string[] = [];
let currentDir = resolvedCwd;
const ultimateStopDirKey = projectRoot
? normalizePath(path.dirname(projectRoot))
: normalizePath(path.dirname(resolvedHome));
while (currentDir && currentDir !== path.dirname(currentDir)) {
if (normalizePath(currentDir) === globalGeminiDirKey) {
break;
}
const potentialPath = toAbsolutePath(
path.join(currentDir, geminiMdFilename),
);
try {
await fs.access(potentialPath, fsSync.constants.R_OK);
if (normalizePath(potentialPath) !== globalMemoryKey) {
upwardPaths.unshift(potentialPath);
}
} catch {
// Not found, continue.
}
if (normalizePath(currentDir) === ultimateStopDirKey) {
break;
}
currentDir = path.dirname(currentDir);
}
upwardPaths.forEach((p) => projectPaths.add(p));
const mergedOptions: FileFilteringOptions = {
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
...fileFilteringOptions,
};
const downwardPaths = await bfsFileSearch(resolvedCwd, {
fileName: geminiMdFilename,
maxDirs,
fileService,
fileFilteringOptions: mergedOptions,
});
downwardPaths.sort();
for (const dPath of downwardPaths) {
projectPaths.add(toAbsolutePath(dPath));
}
}
}
return {
global: Array.from(globalPaths),
project: Array.from(projectPaths),
};
}
export async function readGeminiMdFiles(
filePaths: string[],
importFormat: 'flat' | 'tree' = 'tree',
@@ -680,158 +509,6 @@ async function findUpwardGeminiFiles(
return upwardPaths;
}
export interface LoadServerHierarchicalMemoryResponse {
memoryContent: HierarchicalMemory;
fileCount: number;
filePaths: string[];
}
/**
* Loads hierarchical GEMINI.md files and concatenates their content.
* This function is intended for use by the server.
*/
export async function loadServerHierarchicalMemory(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[],
fileService: FileDiscoveryService,
extensionLoader: ExtensionLoader,
folderTrust: boolean,
importFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions,
maxDirs: number = 200,
boundaryMarkers: readonly string[] = ['.git'],
): Promise<LoadServerHierarchicalMemoryResponse> {
// FIX: Use real, canonical paths for a reliable comparison to handle symlinks.
const realCwd = normalizePath(
await fs.realpath(path.resolve(currentWorkingDirectory)),
);
const realHome = normalizePath(await fs.realpath(path.resolve(homedir())));
const isHomeDirectory = realCwd === realHome;
// If it is the home directory, pass an empty string to the core memory
// function to signal that it should skip the workspace search.
currentWorkingDirectory = isHomeDirectory ? '' : currentWorkingDirectory;
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] Loading server hierarchical memory for CWD:',
currentWorkingDirectory,
`(importFormat: ${importFormat})`,
);
// For the server, homedir() refers to the server process's home.
// This is consistent with how MemoryTool already finds the global path.
const userHomePath = homedir();
// 1. SCATTER: Gather all paths
const [discoveryResult, extensionPaths] = await Promise.all([
getGeminiMdFilePathsInternal(
currentWorkingDirectory,
includeDirectoriesToReadGemini,
userHomePath,
fileService,
folderTrust,
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
maxDirs,
boundaryMarkers,
),
Promise.resolve(getExtensionMemoryPaths(extensionLoader)),
]);
const allFilePathsStringDeduped = Array.from(
new Set([
...discoveryResult.global,
...discoveryResult.project,
...extensionPaths,
]),
);
if (allFilePathsStringDeduped.length === 0) {
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] No GEMINI.md files found in hierarchy of the workspace.',
);
return {
memoryContent: { global: '', extension: '', project: '' },
fileCount: 0,
filePaths: [],
};
}
// deduplicate by file identity to handle case-insensitive filesystems
const { paths: allFilePaths } = await deduplicatePathsByFileIdentity(
allFilePathsStringDeduped,
);
if (allFilePaths.length === 0) {
debugLogger.debug(
'[DEBUG] [MemoryDiscovery] No unique GEMINI.md files found after deduplication by file identity.',
);
return {
memoryContent: { global: '', extension: '', project: '' },
fileCount: 0,
filePaths: [],
};
}
// 2. GATHER: Read all files in parallel
const allContents = await readGeminiMdFiles(
allFilePaths,
importFormat,
boundaryMarkers,
);
const contentsMap = new Map(allContents.map((c) => [c.filePath, c]));
// 3. CATEGORIZE: Back into Global, Project, Extension
const hierarchicalMemory = categorizeAndConcatenate(
{
global: discoveryResult.global,
extension: extensionPaths,
project: discoveryResult.project,
},
contentsMap,
);
return {
memoryContent: hierarchicalMemory,
fileCount: allContents.filter((c) => c.content !== null).length,
filePaths: allFilePaths,
};
}
/**
* Loads the hierarchical memory and resets the state of `config` as needed such
* that it reflects the new memory.
*
* Returns the result of the call to `loadHierarchicalGeminiMemory`.
*/
export async function refreshServerHierarchicalMemory(config: Config) {
const result = await loadServerHierarchicalMemory(
config.getWorkingDir(),
config.shouldLoadMemoryFromIncludeDirectories()
? config.getWorkspaceContext().getDirectories()
: [],
config.getFileService(),
config.getExtensionLoader(),
config.isTrustedFolder(),
config.getImportFormat(),
config.getFileFilteringOptions(),
config.getDiscoveryMaxDirs(),
config.getMemoryBoundaryMarkers(),
);
const mcpInstructions =
config.getMcpClientManager()?.getMcpInstructions() || '';
const finalMemory: HierarchicalMemory = {
...result.memoryContent,
project: [result.memoryContent.project, mcpInstructions.trimStart()]
.filter(Boolean)
.join('\n\n'),
};
config.setUserMemory(finalMemory);
config.setGeminiMdFileCount(result.fileCount);
config.setGeminiMdFilePaths(result.filePaths);
coreEvents.emit(CoreEvent.MemoryChanged, { fileCount: result.fileCount });
return result;
}
export async function loadJitSubdirectoryMemory(
targetPath: string,
trustedRoots: string[],