mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): deduplicate GEMINI.md files by device/inode on case-insensitive filesystems (#19904) (#19915)
This commit is contained in:
@@ -120,7 +120,6 @@ export async function loadConfig(
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
[workspaceDir],
|
[workspaceDir],
|
||||||
false,
|
|
||||||
fileService,
|
fileService,
|
||||||
extensionLoader,
|
extensionLoader,
|
||||||
folderTrust,
|
folderTrust,
|
||||||
|
|||||||
@@ -116,14 +116,16 @@ vi.mock('@google/gemini-cli-core', async () => {
|
|||||||
(
|
(
|
||||||
cwd,
|
cwd,
|
||||||
dirs,
|
dirs,
|
||||||
debug,
|
|
||||||
fileService,
|
fileService,
|
||||||
extensionLoader: ExtensionLoader,
|
extensionLoader: ExtensionLoader,
|
||||||
|
_folderTrust,
|
||||||
|
_importFormat,
|
||||||
|
_fileFilteringOptions,
|
||||||
_maxDirs,
|
_maxDirs,
|
||||||
) => {
|
) => {
|
||||||
const extensionPaths = extensionLoader
|
const extensionPaths =
|
||||||
.getExtensions()
|
extensionLoader?.getExtensions?.()?.flatMap((e) => e.contextFiles) ||
|
||||||
.flatMap((e) => e.contextFiles);
|
[];
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
memoryContent: extensionPaths.join(',') || '',
|
memoryContent: extensionPaths.join(',') || '',
|
||||||
fileCount: extensionPaths?.length || 0,
|
fileCount: extensionPaths?.length || 0,
|
||||||
@@ -847,7 +849,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
expect.any(ExtensionManager),
|
expect.any(ExtensionManager),
|
||||||
true,
|
true,
|
||||||
@@ -876,7 +877,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[includeDir],
|
[includeDir],
|
||||||
false,
|
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
expect.any(ExtensionManager),
|
expect.any(ExtensionManager),
|
||||||
true,
|
true,
|
||||||
@@ -904,7 +904,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
expect.any(ExtensionManager),
|
expect.any(ExtensionManager),
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -499,7 +499,6 @@ export async function loadCliConfig(
|
|||||||
settings.context?.loadMemoryFromIncludeDirectories || false
|
settings.context?.loadMemoryFromIncludeDirectories || false
|
||||||
? includeDirectories
|
? includeDirectories
|
||||||
: [],
|
: [],
|
||||||
debugMode,
|
|
||||||
fileService,
|
fileService,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
trustedFolder,
|
trustedFolder,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ vi.mock('../utils/memoryDiscovery.js', async (importOriginal) => {
|
|||||||
getEnvironmentMemoryPaths: vi.fn(),
|
getEnvironmentMemoryPaths: vi.fn(),
|
||||||
readGeminiMdFiles: vi.fn(),
|
readGeminiMdFiles: vi.fn(),
|
||||||
loadJitSubdirectoryMemory: vi.fn(),
|
loadJitSubdirectoryMemory: vi.fn(),
|
||||||
|
deduplicatePathsByFileIdentity: vi.fn(),
|
||||||
concatenateInstructions: vi
|
concatenateInstructions: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(actual.concatenateInstructions),
|
.mockImplementation(actual.concatenateInstructions),
|
||||||
@@ -33,7 +34,6 @@ describe('ContextManager', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
getDebugMode: vi.fn().mockReturnValue(false),
|
|
||||||
getWorkingDir: vi.fn().mockReturnValue('/app'),
|
getWorkingDir: vi.fn().mockReturnValue('/app'),
|
||||||
getImportFormat: vi.fn().mockReturnValue('tree'),
|
getImportFormat: vi.fn().mockReturnValue('tree'),
|
||||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||||
@@ -52,6 +52,13 @@ describe('ContextManager', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.spyOn(coreEvents, 'emit');
|
vi.spyOn(coreEvents, 'emit');
|
||||||
vi.mocked(memoryDiscovery.getExtensionMemoryPaths).mockReturnValue([]);
|
vi.mocked(memoryDiscovery.getExtensionMemoryPaths).mockReturnValue([]);
|
||||||
|
// default mock: deduplication returns paths as-is (no deduplication)
|
||||||
|
vi.mocked(
|
||||||
|
memoryDiscovery.deduplicatePathsByFileIdentity,
|
||||||
|
).mockImplementation(async (paths: string[]) => ({
|
||||||
|
paths,
|
||||||
|
identityMap: new Map<string, string>(),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('refresh', () => {
|
describe('refresh', () => {
|
||||||
@@ -74,13 +81,11 @@ describe('ContextManager', () => {
|
|||||||
await contextManager.refresh();
|
await contextManager.refresh();
|
||||||
|
|
||||||
expect(memoryDiscovery.getGlobalMemoryPaths).toHaveBeenCalled();
|
expect(memoryDiscovery.getGlobalMemoryPaths).toHaveBeenCalled();
|
||||||
expect(memoryDiscovery.getEnvironmentMemoryPaths).toHaveBeenCalledWith(
|
expect(memoryDiscovery.getEnvironmentMemoryPaths).toHaveBeenCalledWith([
|
||||||
['/app'],
|
'/app',
|
||||||
false,
|
]);
|
||||||
);
|
|
||||||
expect(memoryDiscovery.readGeminiMdFiles).toHaveBeenCalledWith(
|
expect(memoryDiscovery.readGeminiMdFiles).toHaveBeenCalledWith(
|
||||||
expect.arrayContaining([...globalPaths, ...envPaths]),
|
expect.arrayContaining([...globalPaths, ...envPaths]),
|
||||||
false,
|
|
||||||
'tree',
|
'tree',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -128,6 +133,50 @@ describe('ContextManager', () => {
|
|||||||
expect(contextManager.getEnvironmentMemory()).toBe('');
|
expect(contextManager.getEnvironmentMemory()).toBe('');
|
||||||
expect(contextManager.getGlobalMemory()).toContain('Global Content');
|
expect(contextManager.getGlobalMemory()).toContain('Global Content');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should deduplicate files by file identity in case-insensitive filesystems', async () => {
|
||||||
|
const globalPaths = ['/home/user/.gemini/GEMINI.md'];
|
||||||
|
const envPaths = ['/app/gemini.md', '/app/GEMINI.md'];
|
||||||
|
|
||||||
|
vi.mocked(memoryDiscovery.getGlobalMemoryPaths).mockResolvedValue(
|
||||||
|
globalPaths,
|
||||||
|
);
|
||||||
|
vi.mocked(memoryDiscovery.getEnvironmentMemoryPaths).mockResolvedValue(
|
||||||
|
envPaths,
|
||||||
|
);
|
||||||
|
|
||||||
|
// mock deduplication to return deduplicated paths (simulating same file)
|
||||||
|
vi.mocked(
|
||||||
|
memoryDiscovery.deduplicatePathsByFileIdentity,
|
||||||
|
).mockResolvedValue({
|
||||||
|
paths: ['/home/user/.gemini/GEMINI.md', '/app/gemini.md'],
|
||||||
|
identityMap: new Map<string, string>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(memoryDiscovery.readGeminiMdFiles).mockResolvedValue([
|
||||||
|
{ filePath: '/home/user/.gemini/GEMINI.md', content: 'Global Content' },
|
||||||
|
{ filePath: '/app/gemini.md', content: 'Project Content' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await contextManager.refresh();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
memoryDiscovery.deduplicatePathsByFileIdentity,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'/home/user/.gemini/GEMINI.md',
|
||||||
|
'/app/gemini.md',
|
||||||
|
'/app/GEMINI.md',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(memoryDiscovery.readGeminiMdFiles).toHaveBeenCalledWith(
|
||||||
|
['/home/user/.gemini/GEMINI.md', '/app/gemini.md'],
|
||||||
|
'tree',
|
||||||
|
);
|
||||||
|
expect(contextManager.getEnvironmentMemory()).toContain(
|
||||||
|
'Project Content',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discoverContext', () => {
|
describe('discoverContext', () => {
|
||||||
@@ -147,7 +196,7 @@ describe('ContextManager', () => {
|
|||||||
'/app/src/file.ts',
|
'/app/src/file.ts',
|
||||||
['/app'],
|
['/app'],
|
||||||
expect.any(Set),
|
expect.any(Set),
|
||||||
false,
|
expect.any(Set),
|
||||||
);
|
);
|
||||||
expect(result).toMatch(/--- Context from: src[\\/]GEMINI\.md ---/);
|
expect(result).toMatch(/--- Context from: src[\\/]GEMINI\.md ---/);
|
||||||
expect(result).toContain('Src Content');
|
expect(result).toContain('Src Content');
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
readGeminiMdFiles,
|
readGeminiMdFiles,
|
||||||
categorizeAndConcatenate,
|
categorizeAndConcatenate,
|
||||||
type GeminiFileContent,
|
type GeminiFileContent,
|
||||||
|
deduplicatePathsByFileIdentity,
|
||||||
} from '../utils/memoryDiscovery.js';
|
} from '../utils/memoryDiscovery.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||||
|
|
||||||
export class ContextManager {
|
export class ContextManager {
|
||||||
private readonly loadedPaths: Set<string> = new Set();
|
private readonly loadedPaths: Set<string> = new Set();
|
||||||
|
private readonly loadedFileIdentities: Set<string> = new Set();
|
||||||
private readonly config: Config;
|
private readonly config: Config;
|
||||||
private globalMemory: string = '';
|
private globalMemory: string = '';
|
||||||
private extensionMemory: string = '';
|
private extensionMemory: string = '';
|
||||||
@@ -33,49 +35,61 @@ export class ContextManager {
|
|||||||
*/
|
*/
|
||||||
async refresh(): Promise<void> {
|
async refresh(): Promise<void> {
|
||||||
this.loadedPaths.clear();
|
this.loadedPaths.clear();
|
||||||
const debugMode = this.config.getDebugMode();
|
this.loadedFileIdentities.clear();
|
||||||
|
|
||||||
const paths = await this.discoverMemoryPaths(debugMode);
|
const paths = await this.discoverMemoryPaths();
|
||||||
const contentsMap = await this.loadMemoryContents(paths, debugMode);
|
const contentsMap = await this.loadMemoryContents(paths);
|
||||||
|
|
||||||
this.categorizeMemoryContents(paths, contentsMap);
|
this.categorizeMemoryContents(paths, contentsMap);
|
||||||
this.emitMemoryChanged();
|
this.emitMemoryChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async discoverMemoryPaths(debugMode: boolean) {
|
private async discoverMemoryPaths() {
|
||||||
const [global, extension, project] = await Promise.all([
|
const [global, extension, project] = await Promise.all([
|
||||||
getGlobalMemoryPaths(debugMode),
|
getGlobalMemoryPaths(),
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
getExtensionMemoryPaths(this.config.getExtensionLoader()),
|
getExtensionMemoryPaths(this.config.getExtensionLoader()),
|
||||||
),
|
),
|
||||||
this.config.isTrustedFolder()
|
this.config.isTrustedFolder()
|
||||||
? getEnvironmentMemoryPaths(
|
? getEnvironmentMemoryPaths([
|
||||||
[...this.config.getWorkspaceContext().getDirectories()],
|
...this.config.getWorkspaceContext().getDirectories(),
|
||||||
debugMode,
|
])
|
||||||
)
|
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { global, extension, project };
|
return { global, extension, project };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMemoryContents(
|
private async loadMemoryContents(paths: {
|
||||||
paths: { global: string[]; extension: string[]; project: string[] },
|
global: string[];
|
||||||
debugMode: boolean,
|
extension: string[];
|
||||||
) {
|
project: string[];
|
||||||
const allPaths = Array.from(
|
}) {
|
||||||
|
const allPathsStringDeduped = Array.from(
|
||||||
new Set([...paths.global, ...paths.extension, ...paths.project]),
|
new Set([...paths.global, ...paths.extension, ...paths.project]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// deduplicate by file identity to handle case-insensitive filesystems
|
||||||
|
const { paths: allPaths, identityMap: pathIdentityMap } =
|
||||||
|
await deduplicatePathsByFileIdentity(allPathsStringDeduped);
|
||||||
|
|
||||||
const allContents = await readGeminiMdFiles(
|
const allContents = await readGeminiMdFiles(
|
||||||
allPaths,
|
allPaths,
|
||||||
debugMode,
|
|
||||||
this.config.getImportFormat(),
|
this.config.getImportFormat(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.markAsLoaded(
|
const loadedFilePaths = allContents
|
||||||
allContents.filter((c) => c.content !== null).map((c) => c.filePath),
|
.filter((c) => c.content !== null)
|
||||||
);
|
.map((c) => c.filePath);
|
||||||
|
this.markAsLoaded(loadedFilePaths);
|
||||||
|
|
||||||
|
// Cache file identities for performance optimization
|
||||||
|
for (const filePath of loadedFilePaths) {
|
||||||
|
const identity = pathIdentityMap.get(filePath);
|
||||||
|
if (identity) {
|
||||||
|
this.loadedFileIdentities.add(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Map(allContents.map((c) => [c.filePath, c]));
|
return new Map(allContents.map((c) => [c.filePath, c]));
|
||||||
}
|
}
|
||||||
@@ -123,14 +137,22 @@ export class ContextManager {
|
|||||||
accessedPath,
|
accessedPath,
|
||||||
trustedRoots,
|
trustedRoots,
|
||||||
this.loadedPaths,
|
this.loadedPaths,
|
||||||
this.config.getDebugMode(),
|
this.loadedFileIdentities,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.files.length === 0) {
|
if (result.files.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.markAsLoaded(result.files.map((f) => f.path));
|
const newFilePaths = result.files.map((f) => f.path);
|
||||||
|
this.markAsLoaded(newFilePaths);
|
||||||
|
|
||||||
|
// Cache identities for newly loaded files
|
||||||
|
if (result.fileIdentities) {
|
||||||
|
for (const identity of result.fileIdentities) {
|
||||||
|
this.loadedFileIdentities.add(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
return concatenateInstructions(
|
return concatenateInstructions(
|
||||||
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
||||||
this.config.getWorkingDir(),
|
this.config.getWorkingDir(),
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import { Config, type GeminiCLIExtension } from '../config/config.js';
|
|||||||
import { Storage } from '../config/storage.js';
|
import { Storage } from '../config/storage.js';
|
||||||
import { SimpleExtensionLoader } from './extensionLoader.js';
|
import { SimpleExtensionLoader } from './extensionLoader.js';
|
||||||
import { CoreEvent, coreEvents } from './events.js';
|
import { CoreEvent, coreEvents } from './events.js';
|
||||||
import { debugLogger } from './debugLogger.js';
|
|
||||||
|
|
||||||
vi.mock('os', async (importOriginal) => {
|
vi.mock('os', async (importOriginal) => {
|
||||||
const actualOs = await importOriginal<typeof os>();
|
const actualOs = await importOriginal<typeof os>();
|
||||||
@@ -129,7 +128,6 @@ describe('memoryDiscovery', () => {
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
false, // untrusted
|
false, // untrusted
|
||||||
@@ -166,7 +164,6 @@ describe('memoryDiscovery', () => {
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
false, // untrusted
|
false, // untrusted
|
||||||
@@ -184,7 +181,6 @@ describe('memoryDiscovery', () => {
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -208,7 +204,6 @@ describe('memoryDiscovery', () => {
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -241,7 +236,6 @@ default context content
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -275,7 +269,6 @@ custom context content
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -313,7 +306,6 @@ cwd context content
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -348,7 +340,6 @@ Subdir custom memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -383,7 +374,6 @@ Src directory memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -430,7 +420,6 @@ Subdir memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -487,7 +476,6 @@ Subdir memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -512,10 +500,6 @@ My code memory
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should respect the maxDirs parameter during downward scan', async () => {
|
it('should respect the maxDirs parameter during downward scan', async () => {
|
||||||
const consoleDebugSpy = vi
|
|
||||||
.spyOn(debugLogger, 'debug')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
// Create directories in parallel for better performance
|
// Create directories in parallel for better performance
|
||||||
const dirPromises = Array.from({ length: 2 }, (_, i) =>
|
const dirPromises = Array.from({ length: 2 }, (_, i) =>
|
||||||
createEmptyDir(path.join(cwd, `deep_dir_${i}`)),
|
createEmptyDir(path.join(cwd, `deep_dir_${i}`)),
|
||||||
@@ -526,7 +510,6 @@ My code memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
true,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -539,18 +522,13 @@ My code memory
|
|||||||
1, // maxDirs
|
1, // maxDirs
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(consoleDebugSpy).toHaveBeenCalledWith(
|
// Note: bfsFileSearch debug logging is no longer controlled via debugMode parameter
|
||||||
expect.stringContaining('[DEBUG] [BfsFileSearch]'),
|
// The test verifies maxDirs is respected by checking the result, not debug logs
|
||||||
expect.stringContaining('Scanning [1/1]:'),
|
|
||||||
);
|
|
||||||
|
|
||||||
consoleDebugSpy.mockRestore();
|
|
||||||
|
|
||||||
const result = flattenResult(
|
const result = flattenResult(
|
||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -574,7 +552,6 @@ My code memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([
|
new SimpleExtensionLoader([
|
||||||
{
|
{
|
||||||
@@ -609,7 +586,6 @@ Extension memory content
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
[includedDir],
|
[includedDir],
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -647,7 +623,6 @@ included directory memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
createdFiles.map((f) => path.dirname(f)),
|
createdFiles.map((f) => path.dirname(f)),
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -685,7 +660,6 @@ included directory memory
|
|||||||
await loadServerHierarchicalMemory(
|
await loadServerHierarchicalMemory(
|
||||||
parentDir,
|
parentDir,
|
||||||
[childDir, parentDir], // Deliberately include duplicates
|
[childDir, parentDir], // Deliberately include duplicates
|
||||||
false,
|
|
||||||
new FileDiscoveryService(projectRoot),
|
new FileDiscoveryService(projectRoot),
|
||||||
new SimpleExtensionLoader([]),
|
new SimpleExtensionLoader([]),
|
||||||
DEFAULT_FOLDER_TRUST,
|
DEFAULT_FOLDER_TRUST,
|
||||||
@@ -864,6 +838,173 @@ included directory memory
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('case-insensitive filesystem deduplication', () => {
|
||||||
|
it('should deduplicate files that point to the same inode (same physical file)', async () => {
|
||||||
|
const geminiFile = await createTestFile(
|
||||||
|
path.join(projectRoot, 'gemini.md'),
|
||||||
|
'Project root memory',
|
||||||
|
);
|
||||||
|
|
||||||
|
// create hard link to simulate case-insensitive filesystem behavior
|
||||||
|
const geminiFileLink = path.join(projectRoot, 'GEMINI.md');
|
||||||
|
try {
|
||||||
|
await fsPromises.link(geminiFile, geminiFileLink);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
errorMessage.includes('cross-device') ||
|
||||||
|
errorMessage.includes('EXDEV') ||
|
||||||
|
errorMessage.includes('EEXIST')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats1 = await fsPromises.lstat(geminiFile);
|
||||||
|
const stats2 = await fsPromises.lstat(geminiFileLink);
|
||||||
|
expect(stats1.ino).toBe(stats2.ino);
|
||||||
|
expect(stats1.dev).toBe(stats2.dev);
|
||||||
|
|
||||||
|
setGeminiMdFilename(['GEMINI.md', 'gemini.md']);
|
||||||
|
|
||||||
|
const result = flattenResult(
|
||||||
|
await loadServerHierarchicalMemory(
|
||||||
|
cwd,
|
||||||
|
[],
|
||||||
|
new FileDiscoveryService(projectRoot),
|
||||||
|
new SimpleExtensionLoader([]),
|
||||||
|
DEFAULT_FOLDER_TRUST,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle case where files have different inodes (different files)', async () => {
|
||||||
|
const geminiFileLower = await createTestFile(
|
||||||
|
path.join(projectRoot, 'gemini.md'),
|
||||||
|
'Lowercase file content',
|
||||||
|
);
|
||||||
|
const geminiFileUpper = await createTestFile(
|
||||||
|
path.join(projectRoot, 'GEMINI.md'),
|
||||||
|
'Uppercase file content',
|
||||||
|
);
|
||||||
|
|
||||||
|
const stats1 = await fsPromises.lstat(geminiFileLower);
|
||||||
|
const stats2 = await fsPromises.lstat(geminiFileUpper);
|
||||||
|
|
||||||
|
if (stats1.ino !== stats2.ino || stats1.dev !== stats2.dev) {
|
||||||
|
setGeminiMdFilename(['GEMINI.md', 'gemini.md']);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle files that cannot be stat'd (missing files)", async () => {
|
||||||
|
await createTestFile(
|
||||||
|
path.join(projectRoot, 'gemini.md'),
|
||||||
|
'Valid file content',
|
||||||
|
);
|
||||||
|
|
||||||
|
setGeminiMdFilename(['gemini.md', 'missing.md']);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate multiple paths pointing to same file (3+ duplicates)', async () => {
|
||||||
|
const geminiFile = await createTestFile(
|
||||||
|
path.join(projectRoot, 'gemini.md'),
|
||||||
|
'Project root memory',
|
||||||
|
);
|
||||||
|
|
||||||
|
const link1 = path.join(projectRoot, 'GEMINI.md');
|
||||||
|
const link2 = path.join(projectRoot, 'Gemini.md');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsPromises.link(geminiFile, link1);
|
||||||
|
await fsPromises.link(geminiFile, link2);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
errorMessage.includes('cross-device') ||
|
||||||
|
errorMessage.includes('EXDEV') ||
|
||||||
|
errorMessage.includes('EEXIST')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats1 = await fsPromises.lstat(geminiFile);
|
||||||
|
const stats2 = await fsPromises.lstat(link1);
|
||||||
|
const stats3 = await fsPromises.lstat(link2);
|
||||||
|
expect(stats1.ino).toBe(stats2.ino);
|
||||||
|
expect(stats1.ino).toBe(stats3.ino);
|
||||||
|
|
||||||
|
setGeminiMdFilename(['gemini.md', 'GEMINI.md', 'Gemini.md']);
|
||||||
|
|
||||||
|
const result = flattenResult(
|
||||||
|
await loadServerHierarchicalMemory(
|
||||||
|
cwd,
|
||||||
|
[],
|
||||||
|
new FileDiscoveryService(projectRoot),
|
||||||
|
new SimpleExtensionLoader([]),
|
||||||
|
DEFAULT_FOLDER_TRUST,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
await fsPromises.unlink(link2);
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadJitSubdirectoryMemory', () => {
|
describe('loadJitSubdirectoryMemory', () => {
|
||||||
it('should load JIT memory when target is inside a trusted root', async () => {
|
it('should load JIT memory when target is inside a trusted root', async () => {
|
||||||
const rootDir = await createEmptyDir(path.join(testRootDir, 'jit_root'));
|
const rootDir = await createEmptyDir(path.join(testRootDir, 'jit_root'));
|
||||||
@@ -937,6 +1078,57 @@ included directory memory
|
|||||||
expect(result.files[0].content).toBe('Subdir content');
|
expect(result.files[0].content).toBe('Subdir content');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should deduplicate files in JIT memory loading (same inode)', async () => {
|
||||||
|
const rootDir = await createEmptyDir(path.join(testRootDir, 'jit_root'));
|
||||||
|
const subDir = await createEmptyDir(path.join(rootDir, 'subdir'));
|
||||||
|
const targetFile = path.join(subDir, 'target.txt');
|
||||||
|
|
||||||
|
const geminiFile = await createTestFile(
|
||||||
|
path.join(subDir, 'gemini.md'),
|
||||||
|
'JIT memory content',
|
||||||
|
);
|
||||||
|
|
||||||
|
const geminiFileLink = path.join(subDir, 'GEMINI.md');
|
||||||
|
try {
|
||||||
|
await fsPromises.link(geminiFile, geminiFileLink);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
errorMessage.includes('cross-device') ||
|
||||||
|
errorMessage.includes('EXDEV') ||
|
||||||
|
errorMessage.includes('EEXIST')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats1 = await fsPromises.lstat(geminiFile);
|
||||||
|
const stats2 = await fsPromises.lstat(geminiFileLink);
|
||||||
|
expect(stats1.ino).toBe(stats2.ino);
|
||||||
|
|
||||||
|
setGeminiMdFilename(['gemini.md', 'GEMINI.md']);
|
||||||
|
|
||||||
|
const result = await loadJitSubdirectoryMemory(
|
||||||
|
targetFile,
|
||||||
|
[rootDir],
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.files).toHaveLength(1);
|
||||||
|
expect(result.files[0].content).toBe('JIT memory content');
|
||||||
|
const contentMatches =
|
||||||
|
result.files[0].content.match(/JIT memory content/g);
|
||||||
|
expect(contentMatches).toHaveLength(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsPromises.unlink(geminiFileLink);
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should use the deepest trusted root when multiple nested roots exist', async () => {
|
it('should use the deepest trusted root when multiple nested roots exist', async () => {
|
||||||
const outerRoot = await createEmptyDir(path.join(testRootDir, 'outer'));
|
const outerRoot = await createEmptyDir(path.join(testRootDir, 'outer'));
|
||||||
const innerRoot = await createEmptyDir(path.join(outerRoot, 'inner'));
|
const innerRoot = await createEmptyDir(path.join(outerRoot, 'inner'));
|
||||||
@@ -981,7 +1173,6 @@ included directory memory
|
|||||||
config.shouldLoadMemoryFromIncludeDirectories()
|
config.shouldLoadMemoryFromIncludeDirectories()
|
||||||
? config.getWorkspaceContext().getDirectories()
|
? config.getWorkspaceContext().getDirectories()
|
||||||
: [],
|
: [],
|
||||||
config.getDebugMode(),
|
|
||||||
config.getFileService(),
|
config.getFileService(),
|
||||||
config.getExtensionLoader(),
|
config.getExtensionLoader(),
|
||||||
config.isTrustedFolder(),
|
config.isTrustedFolder(),
|
||||||
@@ -1026,7 +1217,6 @@ included directory memory
|
|||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
getWorkingDir: vi.fn().mockReturnValue(cwd),
|
getWorkingDir: vi.fn().mockReturnValue(cwd),
|
||||||
shouldLoadMemoryFromIncludeDirectories: vi.fn().mockReturnValue(false),
|
shouldLoadMemoryFromIncludeDirectories: vi.fn().mockReturnValue(false),
|
||||||
getDebugMode: vi.fn().mockReturnValue(false),
|
|
||||||
getFileService: vi
|
getFileService: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(new FileDiscoveryService(projectRoot)),
|
.mockReturnValue(new FileDiscoveryService(projectRoot)),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { debugLogger } from './debugLogger.js';
|
|||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import type { HierarchicalMemory } from '../config/memory.js';
|
import type { HierarchicalMemory } from '../config/memory.js';
|
||||||
import { CoreEvent, coreEvents } from './events.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
|
// Simple console logger, similar to the one previously in CLI's config.ts
|
||||||
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
||||||
@@ -41,6 +42,110 @@ export interface GeminiFileContent {
|
|||||||
content: string | null;
|
content: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicates file paths by file identity (device + inode) rather than string path.
|
||||||
|
* This is necessary on case-insensitive filesystems where different case variants
|
||||||
|
* of the same filename resolve to the same physical file but have different path strings.
|
||||||
|
*
|
||||||
|
* @param filePaths Array of file paths to deduplicate
|
||||||
|
* @returns Object containing deduplicated file paths and a map of path to identity key
|
||||||
|
*/
|
||||||
|
export async function deduplicatePathsByFileIdentity(
|
||||||
|
filePaths: string[],
|
||||||
|
): Promise<{
|
||||||
|
paths: string[];
|
||||||
|
identityMap: Map<string, string>;
|
||||||
|
}> {
|
||||||
|
if (filePaths.length === 0) {
|
||||||
|
return {
|
||||||
|
paths: [],
|
||||||
|
identityMap: new Map<string, string>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// first deduplicate by string path to avoid redundant stat calls
|
||||||
|
const uniqueFilePaths = Array.from(new Set(filePaths));
|
||||||
|
|
||||||
|
const fileIdentityMap = new Map<string, string>();
|
||||||
|
const deduplicatedPaths: string[] = [];
|
||||||
|
|
||||||
|
const CONCURRENT_LIMIT = 20;
|
||||||
|
const results: Array<{
|
||||||
|
path: string;
|
||||||
|
dev: bigint | number | null;
|
||||||
|
ino: bigint | number | null;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < uniqueFilePaths.length; i += CONCURRENT_LIMIT) {
|
||||||
|
const batch = uniqueFilePaths.slice(i, i + CONCURRENT_LIMIT);
|
||||||
|
const batchPromises = batch.map(async (filePath) => {
|
||||||
|
try {
|
||||||
|
// use stat() instead of lstat() to follow symlinks and get target file identity
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
dev: stats.dev,
|
||||||
|
ino: stats.ino,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.debug(
|
||||||
|
`could not stat file for deduplication: ${filePath}. error: ${message}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
dev: null,
|
||||||
|
ino: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchResults = await Promise.allSettled(batchPromises);
|
||||||
|
for (const result of batchResults) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
results.push(result.value);
|
||||||
|
} else {
|
||||||
|
const message = getErrorMessage(result.reason);
|
||||||
|
debugLogger.debug(
|
||||||
|
'[DEBUG] [MemoryDiscovery] unexpected error during deduplication stat:',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToIdentityMap = new Map<string, string>();
|
||||||
|
for (const { path, dev, ino } of results) {
|
||||||
|
if (dev !== null && ino !== null) {
|
||||||
|
const identityKey = `${dev.toString()}:${ino.toString()}`;
|
||||||
|
pathToIdentityMap.set(path, identityKey);
|
||||||
|
if (!fileIdentityMap.has(identityKey)) {
|
||||||
|
fileIdentityMap.set(identityKey, path);
|
||||||
|
deduplicatedPaths.push(path);
|
||||||
|
debugLogger.debug(
|
||||||
|
'[DEBUG] [MemoryDiscovery] deduplication: keeping',
|
||||||
|
path,
|
||||||
|
`(dev: ${dev}, ino: ${ino})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const existingPath = fileIdentityMap.get(identityKey);
|
||||||
|
debugLogger.debug(
|
||||||
|
'[DEBUG] [MemoryDiscovery] deduplication: skipping',
|
||||||
|
path,
|
||||||
|
`(same file as ${existingPath})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deduplicatedPaths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths: deduplicatedPaths,
|
||||||
|
identityMap: pathToIdentityMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function findProjectRoot(startDir: string): Promise<string | null> {
|
async function findProjectRoot(startDir: string): Promise<string | null> {
|
||||||
let currentDir = normalizePath(startDir);
|
let currentDir = normalizePath(startDir);
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -91,7 +196,6 @@ async function getGeminiMdFilePathsInternal(
|
|||||||
currentWorkingDirectory: string,
|
currentWorkingDirectory: string,
|
||||||
includeDirectoriesToReadGemini: readonly string[],
|
includeDirectoriesToReadGemini: readonly string[],
|
||||||
userHomePath: string,
|
userHomePath: string,
|
||||||
debugMode: boolean,
|
|
||||||
fileService: FileDiscoveryService,
|
fileService: FileDiscoveryService,
|
||||||
folderTrust: boolean,
|
folderTrust: boolean,
|
||||||
fileFilteringOptions: FileFilteringOptions,
|
fileFilteringOptions: FileFilteringOptions,
|
||||||
@@ -114,7 +218,6 @@ async function getGeminiMdFilePathsInternal(
|
|||||||
getGeminiMdFilePathsInternalForEachDir(
|
getGeminiMdFilePathsInternalForEachDir(
|
||||||
dir,
|
dir,
|
||||||
userHomePath,
|
userHomePath,
|
||||||
debugMode,
|
|
||||||
fileService,
|
fileService,
|
||||||
folderTrust,
|
folderTrust,
|
||||||
fileFilteringOptions,
|
fileFilteringOptions,
|
||||||
@@ -146,7 +249,6 @@ async function getGeminiMdFilePathsInternal(
|
|||||||
async function getGeminiMdFilePathsInternalForEachDir(
|
async function getGeminiMdFilePathsInternalForEachDir(
|
||||||
dir: string,
|
dir: string,
|
||||||
userHomePath: string,
|
userHomePath: string,
|
||||||
debugMode: boolean,
|
|
||||||
fileService: FileDiscoveryService,
|
fileService: FileDiscoveryService,
|
||||||
folderTrust: boolean,
|
folderTrust: boolean,
|
||||||
fileFilteringOptions: FileFilteringOptions,
|
fileFilteringOptions: FileFilteringOptions,
|
||||||
@@ -167,9 +269,10 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||||||
try {
|
try {
|
||||||
await fs.access(globalMemoryPath, fsSync.constants.R_OK);
|
await fs.access(globalMemoryPath, fsSync.constants.R_OK);
|
||||||
globalPaths.add(globalMemoryPath);
|
globalPaths.add(globalMemoryPath);
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Found readable global',
|
||||||
`Found readable global ${geminiMdFilename}: ${globalMemoryPath}`,
|
geminiMdFilename + ':',
|
||||||
|
globalMemoryPath,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// It's okay if it's not found.
|
// It's okay if it's not found.
|
||||||
@@ -179,14 +282,18 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||||||
// if a valid currentWorkingDirectory is provided.
|
// if a valid currentWorkingDirectory is provided.
|
||||||
if (dir && folderTrust) {
|
if (dir && folderTrust) {
|
||||||
const resolvedCwd = normalizePath(dir);
|
const resolvedCwd = normalizePath(dir);
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Searching for',
|
||||||
`Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`,
|
geminiMdFilename,
|
||||||
|
'starting from CWD:',
|
||||||
|
resolvedCwd,
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectRoot = await findProjectRoot(resolvedCwd);
|
const projectRoot = await findProjectRoot(resolvedCwd);
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
|
'[DEBUG] [MemoryDiscovery] Determined project root:',
|
||||||
|
projectRoot ?? 'None',
|
||||||
|
);
|
||||||
|
|
||||||
const upwardPaths: string[] = [];
|
const upwardPaths: string[] = [];
|
||||||
let currentDir = resolvedCwd;
|
let currentDir = resolvedCwd;
|
||||||
@@ -230,7 +337,6 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||||||
const downwardPaths = await bfsFileSearch(resolvedCwd, {
|
const downwardPaths = await bfsFileSearch(resolvedCwd, {
|
||||||
fileName: geminiMdFilename,
|
fileName: geminiMdFilename,
|
||||||
maxDirs,
|
maxDirs,
|
||||||
debug: debugMode,
|
|
||||||
fileService,
|
fileService,
|
||||||
fileFilteringOptions: mergedOptions,
|
fileFilteringOptions: mergedOptions,
|
||||||
});
|
});
|
||||||
@@ -249,7 +355,6 @@ async function getGeminiMdFilePathsInternalForEachDir(
|
|||||||
|
|
||||||
export async function readGeminiMdFiles(
|
export async function readGeminiMdFiles(
|
||||||
filePaths: string[],
|
filePaths: string[],
|
||||||
debugMode: boolean,
|
|
||||||
importFormat: 'flat' | 'tree' = 'tree',
|
importFormat: 'flat' | 'tree' = 'tree',
|
||||||
): Promise<GeminiFileContent[]> {
|
): Promise<GeminiFileContent[]> {
|
||||||
// Process files in parallel with concurrency limit to prevent EMFILE errors
|
// Process files in parallel with concurrency limit to prevent EMFILE errors
|
||||||
@@ -267,14 +372,15 @@ export async function readGeminiMdFiles(
|
|||||||
const processedResult = await processImports(
|
const processedResult = await processImports(
|
||||||
content,
|
content,
|
||||||
path.dirname(filePath),
|
path.dirname(filePath),
|
||||||
debugMode,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
importFormat,
|
importFormat,
|
||||||
);
|
);
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Successfully read and processed imports:',
|
||||||
`Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`,
|
filePath,
|
||||||
|
`(Length: ${processedResult.content.length})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { filePath, content: processedResult.content };
|
return { filePath, content: processedResult.content };
|
||||||
@@ -288,7 +394,10 @@ export async function readGeminiMdFiles(
|
|||||||
`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
|
`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (debugMode) logger.debug(`Failed to read: ${filePath}`);
|
debugLogger.debug(
|
||||||
|
'[DEBUG] [MemoryDiscovery] Failed to read:',
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
return { filePath, content: null }; // Still include it with null content
|
return { filePath, content: null }; // Still include it with null content
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -337,11 +446,10 @@ export function concatenateInstructions(
|
|||||||
|
|
||||||
export interface MemoryLoadResult {
|
export interface MemoryLoadResult {
|
||||||
files: Array<{ path: string; content: string }>;
|
files: Array<{ path: string; content: string }>;
|
||||||
|
fileIdentities?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGlobalMemoryPaths(
|
export async function getGlobalMemoryPaths(): Promise<string[]> {
|
||||||
debugMode: boolean = false,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const userHome = homedir();
|
const userHome = homedir();
|
||||||
const geminiMdFilenames = getAllGeminiMdFilenames();
|
const geminiMdFilenames = getAllGeminiMdFilenames();
|
||||||
|
|
||||||
@@ -349,9 +457,10 @@ export async function getGlobalMemoryPaths(
|
|||||||
const globalPath = normalizePath(path.join(userHome, GEMINI_DIR, filename));
|
const globalPath = normalizePath(path.join(userHome, GEMINI_DIR, filename));
|
||||||
try {
|
try {
|
||||||
await fs.access(globalPath, fsSync.constants.R_OK);
|
await fs.access(globalPath, fsSync.constants.R_OK);
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(`Found global memory file: ${globalPath}`);
|
'[DEBUG] [MemoryDiscovery] Found global memory file:',
|
||||||
}
|
globalPath,
|
||||||
|
);
|
||||||
return globalPath;
|
return globalPath;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -377,19 +486,18 @@ export function getExtensionMemoryPaths(
|
|||||||
|
|
||||||
export async function getEnvironmentMemoryPaths(
|
export async function getEnvironmentMemoryPaths(
|
||||||
trustedRoots: string[],
|
trustedRoots: string[],
|
||||||
debugMode: boolean = false,
|
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const allPaths = new Set<string>();
|
const allPaths = new Set<string>();
|
||||||
|
|
||||||
// Trusted Roots Upward Traversal (Parallelized)
|
// Trusted Roots Upward Traversal (Parallelized)
|
||||||
const traversalPromises = trustedRoots.map(async (root) => {
|
const traversalPromises = trustedRoots.map(async (root) => {
|
||||||
const resolvedRoot = normalizePath(root);
|
const resolvedRoot = normalizePath(root);
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Loading environment memory for trusted root:',
|
||||||
`Loading environment memory for trusted root: ${resolvedRoot} (Stopping exactly here)`,
|
resolvedRoot,
|
||||||
|
'(Stopping exactly here)',
|
||||||
);
|
);
|
||||||
}
|
return findUpwardGeminiFiles(resolvedRoot, resolvedRoot);
|
||||||
return findUpwardGeminiFiles(resolvedRoot, resolvedRoot, debugMode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pathArrays = await Promise.all(traversalPromises);
|
const pathArrays = await Promise.all(traversalPromises);
|
||||||
@@ -427,7 +535,6 @@ export function categorizeAndConcatenate(
|
|||||||
async function findUpwardGeminiFiles(
|
async function findUpwardGeminiFiles(
|
||||||
startDir: string,
|
startDir: string,
|
||||||
stopDir: string,
|
stopDir: string,
|
||||||
debugMode: boolean,
|
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const upwardPaths: string[] = [];
|
const upwardPaths: string[] = [];
|
||||||
let currentDir = normalizePath(startDir);
|
let currentDir = normalizePath(startDir);
|
||||||
@@ -435,11 +542,12 @@ async function findUpwardGeminiFiles(
|
|||||||
const geminiMdFilenames = getAllGeminiMdFilenames();
|
const geminiMdFilenames = getAllGeminiMdFilenames();
|
||||||
const globalGeminiDir = normalizePath(path.join(homedir(), GEMINI_DIR));
|
const globalGeminiDir = normalizePath(path.join(homedir(), GEMINI_DIR));
|
||||||
|
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Starting upward search from',
|
||||||
`Starting upward search from ${currentDir} stopping at ${resolvedStopDir}`,
|
currentDir,
|
||||||
|
'stopping at',
|
||||||
|
resolvedStopDir,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (currentDir === globalGeminiDir) {
|
if (currentDir === globalGeminiDir) {
|
||||||
@@ -485,7 +593,6 @@ export interface LoadServerHierarchicalMemoryResponse {
|
|||||||
export async function loadServerHierarchicalMemory(
|
export async function loadServerHierarchicalMemory(
|
||||||
currentWorkingDirectory: string,
|
currentWorkingDirectory: string,
|
||||||
includeDirectoriesToReadGemini: readonly string[],
|
includeDirectoriesToReadGemini: readonly string[],
|
||||||
debugMode: boolean,
|
|
||||||
fileService: FileDiscoveryService,
|
fileService: FileDiscoveryService,
|
||||||
extensionLoader: ExtensionLoader,
|
extensionLoader: ExtensionLoader,
|
||||||
folderTrust: boolean,
|
folderTrust: boolean,
|
||||||
@@ -504,9 +611,10 @@ export async function loadServerHierarchicalMemory(
|
|||||||
// function to signal that it should skip the workspace search.
|
// function to signal that it should skip the workspace search.
|
||||||
currentWorkingDirectory = isHomeDirectory ? '' : currentWorkingDirectory;
|
currentWorkingDirectory = isHomeDirectory ? '' : currentWorkingDirectory;
|
||||||
|
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Loading server hierarchical memory for CWD:',
|
||||||
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
|
currentWorkingDirectory,
|
||||||
|
`(importFormat: ${importFormat})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// For the server, homedir() refers to the server process's home.
|
// For the server, homedir() refers to the server process's home.
|
||||||
@@ -519,7 +627,6 @@ export async function loadServerHierarchicalMemory(
|
|||||||
currentWorkingDirectory,
|
currentWorkingDirectory,
|
||||||
includeDirectoriesToReadGemini,
|
includeDirectoriesToReadGemini,
|
||||||
userHomePath,
|
userHomePath,
|
||||||
debugMode,
|
|
||||||
fileService,
|
fileService,
|
||||||
folderTrust,
|
folderTrust,
|
||||||
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||||
@@ -528,7 +635,7 @@ export async function loadServerHierarchicalMemory(
|
|||||||
Promise.resolve(getExtensionMemoryPaths(extensionLoader)),
|
Promise.resolve(getExtensionMemoryPaths(extensionLoader)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const allFilePaths = Array.from(
|
const allFilePathsStringDeduped = Array.from(
|
||||||
new Set([
|
new Set([
|
||||||
...discoveryResult.global,
|
...discoveryResult.global,
|
||||||
...discoveryResult.project,
|
...discoveryResult.project,
|
||||||
@@ -536,9 +643,26 @@ export async function loadServerHierarchicalMemory(
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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) {
|
if (allFilePaths.length === 0) {
|
||||||
if (debugMode)
|
debugLogger.debug(
|
||||||
logger.debug('No GEMINI.md files found in hierarchy of the workspace.');
|
'[DEBUG] [MemoryDiscovery] No unique GEMINI.md files found after deduplication by file identity.',
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
memoryContent: { global: '', extension: '', project: '' },
|
memoryContent: { global: '', extension: '', project: '' },
|
||||||
fileCount: 0,
|
fileCount: 0,
|
||||||
@@ -547,11 +671,7 @@ export async function loadServerHierarchicalMemory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. GATHER: Read all files in parallel
|
// 2. GATHER: Read all files in parallel
|
||||||
const allContents = await readGeminiMdFiles(
|
const allContents = await readGeminiMdFiles(allFilePaths, importFormat);
|
||||||
allFilePaths,
|
|
||||||
debugMode,
|
|
||||||
importFormat,
|
|
||||||
);
|
|
||||||
const contentsMap = new Map(allContents.map((c) => [c.filePath, c]));
|
const contentsMap = new Map(allContents.map((c) => [c.filePath, c]));
|
||||||
|
|
||||||
// 3. CATEGORIZE: Back into Global, Project, Extension
|
// 3. CATEGORIZE: Back into Global, Project, Extension
|
||||||
@@ -584,7 +704,6 @@ export async function refreshServerHierarchicalMemory(config: Config) {
|
|||||||
config.shouldLoadMemoryFromIncludeDirectories()
|
config.shouldLoadMemoryFromIncludeDirectories()
|
||||||
? config.getWorkspaceContext().getDirectories()
|
? config.getWorkspaceContext().getDirectories()
|
||||||
: [],
|
: [],
|
||||||
config.getDebugMode(),
|
|
||||||
config.getFileService(),
|
config.getFileService(),
|
||||||
config.getExtensionLoader(),
|
config.getExtensionLoader(),
|
||||||
config.isTrustedFolder(),
|
config.isTrustedFolder(),
|
||||||
@@ -611,7 +730,7 @@ export async function loadJitSubdirectoryMemory(
|
|||||||
targetPath: string,
|
targetPath: string,
|
||||||
trustedRoots: string[],
|
trustedRoots: string[],
|
||||||
alreadyLoadedPaths: Set<string>,
|
alreadyLoadedPaths: Set<string>,
|
||||||
debugMode: boolean = false,
|
alreadyLoadedIdentities?: Set<string>,
|
||||||
): Promise<MemoryLoadResult> {
|
): Promise<MemoryLoadResult> {
|
||||||
const resolvedTarget = normalizePath(targetPath);
|
const resolvedTarget = normalizePath(targetPath);
|
||||||
let bestRoot: string | null = null;
|
let bestRoot: string | null = null;
|
||||||
@@ -634,39 +753,86 @@ export async function loadJitSubdirectoryMemory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!bestRoot) {
|
if (!bestRoot) {
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] JIT memory skipped:',
|
||||||
`JIT memory skipped: ${resolvedTarget} is not in any trusted root.`,
|
resolvedTarget,
|
||||||
|
'is not in any trusted root.',
|
||||||
);
|
);
|
||||||
}
|
return { files: [], fileIdentities: [] };
|
||||||
return { files: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(
|
'[DEBUG] [MemoryDiscovery] Loading JIT memory for',
|
||||||
`Loading JIT memory for ${resolvedTarget} (Trusted root: ${bestRoot})`,
|
resolvedTarget,
|
||||||
|
`(Trusted root: ${bestRoot})`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Traverse from target up to the trusted root
|
// Traverse from target up to the trusted root
|
||||||
const potentialPaths = await findUpwardGeminiFiles(
|
const potentialPaths = await findUpwardGeminiFiles(resolvedTarget, bestRoot);
|
||||||
resolvedTarget,
|
|
||||||
bestRoot,
|
|
||||||
debugMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter out already loaded paths
|
if (potentialPaths.length === 0) {
|
||||||
const newPaths = potentialPaths.filter((p) => !alreadyLoadedPaths.has(p));
|
return { files: [], fileIdentities: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// deduplicate by file identity to handle case-insensitive filesystems
|
||||||
|
// this deduplicates within the current batch
|
||||||
|
const { paths: deduplicatedNewPaths, identityMap: newPathsIdentityMap } =
|
||||||
|
await deduplicatePathsByFileIdentity(potentialPaths);
|
||||||
|
|
||||||
|
// Use cached file identities if provided, otherwise build from paths
|
||||||
|
// This avoids redundant fs.stat() calls on already loaded files
|
||||||
|
const cachedIdentities = alreadyLoadedIdentities ?? new Set<string>();
|
||||||
|
if (!alreadyLoadedIdentities && alreadyLoadedPaths.size > 0) {
|
||||||
|
const CONCURRENT_LIMIT = 20;
|
||||||
|
const alreadyLoadedArray = Array.from(alreadyLoadedPaths);
|
||||||
|
|
||||||
|
for (let i = 0; i < alreadyLoadedArray.length; i += CONCURRENT_LIMIT) {
|
||||||
|
const batch = alreadyLoadedArray.slice(i, i + CONCURRENT_LIMIT);
|
||||||
|
const batchPromises = batch.map(async (filePath) => {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
const identityKey = `${stats.dev.toString()}:${stats.ino.toString()}`;
|
||||||
|
cachedIdentities.add(identityKey);
|
||||||
|
} catch {
|
||||||
|
// ignore errors - if we can't stat it, we can't deduplicate by identity
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Await each batch to properly limit concurrency and prevent EMFILE errors
|
||||||
|
await Promise.allSettled(batchPromises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter out paths that match already loaded files by identity
|
||||||
|
// reuse the identities from deduplicatePathsByFileIdentity to avoid redundant stat calls
|
||||||
|
const newPaths: string[] = [];
|
||||||
|
const newFileIdentities: string[] = [];
|
||||||
|
for (const filePath of deduplicatedNewPaths) {
|
||||||
|
const identityKey = newPathsIdentityMap.get(filePath);
|
||||||
|
if (identityKey && cachedIdentities.has(identityKey)) {
|
||||||
|
debugLogger.debug(
|
||||||
|
'[DEBUG] [MemoryDiscovery] jit memory: skipping',
|
||||||
|
filePath,
|
||||||
|
'(already loaded with different case)',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if we don't have an identity (stat failed), include it to be safe
|
||||||
|
newPaths.push(filePath);
|
||||||
|
if (identityKey) {
|
||||||
|
newFileIdentities.push(identityKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (newPaths.length === 0) {
|
if (newPaths.length === 0) {
|
||||||
return { files: [] };
|
return { files: [], fileIdentities: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugMode) {
|
debugLogger.debug(
|
||||||
logger.debug(`Found new JIT memory files: ${JSON.stringify(newPaths)}`);
|
'[DEBUG] [MemoryDiscovery] Found new JIT memory files:',
|
||||||
}
|
JSON.stringify(newPaths),
|
||||||
|
);
|
||||||
|
|
||||||
const contents = await readGeminiMdFiles(newPaths, debugMode, 'tree');
|
const contents = await readGeminiMdFiles(newPaths, 'tree');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files: contents
|
files: contents
|
||||||
@@ -676,5 +842,6 @@ export async function loadJitSubdirectoryMemory(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
content: item.content as string,
|
content: item.content as string,
|
||||||
})),
|
})),
|
||||||
|
fileIdentities: newFileIdentities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user