diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 76ab870ad..c814633b7 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -241,7 +241,6 @@ Per-field precedence for `generationConfig`: | ------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `context.fileName` | string or array of strings | The name of the context file(s). | `undefined` | | `context.importFormat` | string | The format to use when importing memory. | `undefined` | -| `context.discoveryMaxDirs` | number | Maximum number of directories to search for memory. | `200` | | `context.includeDirectories` | array | Additional directories to include in the workspace context. Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag. | `[]` | | `context.loadFromIncludeDirectories` | boolean | Controls the behavior of the `/memory refresh` command. If set to `true`, `QWEN.md` files should be loaded from all directories that are added. If set to `false`, `QWEN.md` should only be loaded from the current directory. | `false` | | `context.fileFiltering.respectGitIgnore` | boolean | Respect .gitignore files when searching. | `true` | @@ -530,16 +529,13 @@ Here's a conceptual example of what a context file at the root of a TypeScript p This example demonstrates how you can provide general project context, specific coding conventions, and even notes about particular files or components. The more relevant and precise your context files are, the better the AI can assist you. Project-specific context files are highly encouraged to establish conventions and context. -- **Hierarchical Loading and Precedence:** The CLI implements a sophisticated hierarchical memory system by loading context files (e.g., `QWEN.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is: +- **Hierarchical Loading and Precedence:** The CLI implements a hierarchical memory system by loading context files (e.g., `QWEN.md`) from several locations. Content from files lower in this list (more specific) typically overrides or supplements content from files higher up (more general). The exact concatenation order and final context can be inspected using the `/memory show` command. The typical loading order is: 1. **Global Context File:** - Location: `~/.qwen/` (e.g., `~/.qwen/QWEN.md` in your user home directory). - Scope: Provides default instructions for all your projects. 2. **Project Root & Ancestors Context Files:** - Location: The CLI searches for the configured context file in the current working directory and then in each parent directory up to either the project root (identified by a `.git` folder) or your home directory. - Scope: Provides context relevant to the entire project or a significant portion of it. - 3. **Sub-directory Context Files (Contextual/Local):** - - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file. - - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project. - **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context. - **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../configuration/memory). - **Commands for Memory Management:** diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 850d4a822..aa3e8b3b3 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1196,11 +1196,6 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { ], true, 'tree', - { - respectGitIgnore: false, - respectQwenIgnore: true, - }, - undefined, // maxDirs ); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index da88654d2..74d15522b 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -9,7 +9,6 @@ import { AuthType, Config, DEFAULT_QWEN_EMBEDDING_MODEL, - DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, FileDiscoveryService, getCurrentGeminiMdFilename, loadServerHierarchicalMemory, @@ -22,7 +21,6 @@ import { isToolEnabled, SessionService, type ResumedSessionData, - type FileFilteringOptions, type MCPServerConfig, type ToolName, EditTool, @@ -643,7 +641,6 @@ export async function loadHierarchicalGeminiMemory( extensionContextFilePaths: string[] = [], folderTrust: boolean, memoryImportFormat: 'flat' | 'tree' = 'tree', - fileFilteringOptions?: FileFilteringOptions, ): Promise<{ memoryContent: string; fileCount: number }> { // FIX: Use real, canonical paths for a reliable comparison to handle symlinks. const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory)); @@ -669,8 +666,6 @@ export async function loadHierarchicalGeminiMemory( extensionContextFilePaths, folderTrust, memoryImportFormat, - fileFilteringOptions, - settings.context?.discoveryMaxDirs, ); } @@ -740,11 +735,6 @@ export async function loadCliConfig( const fileService = new FileDiscoveryService(cwd); - const fileFiltering = { - ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, - ...settings.context?.fileFiltering, - }; - const includeDirectories = (settings.context?.includeDirectories || []) .map(resolvePath) .concat((argv.includeDirectories || []).map(resolvePath)); @@ -761,7 +751,6 @@ export async function loadCliConfig( extensionContextFilePaths, trustedFolder, memoryImportFormat, - fileFiltering, ); let mcpServers = mergeMcpServers(settings, activeExtensions); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index c9b845cd8..d9e5edb30 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -106,7 +106,6 @@ const MIGRATION_MAP: Record = { mcpServers: 'mcpServers', mcpServerCommand: 'mcp.serverCommand', memoryImportFormat: 'context.importFormat', - memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs', model: 'model.name', preferredEditor: 'general.preferredEditor', sandbox: 'tools.sandbox', diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c98c79ffd..1e72ad48b 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -722,15 +722,6 @@ const SETTINGS_SCHEMA = { description: 'The format to use when importing memory.', showInDialog: false, }, - discoveryMaxDirs: { - type: 'number', - label: 'Memory Discovery Max Dirs', - category: 'Context', - requiresRestart: false, - default: 200, - description: 'Maximum number of directories to search for memory.', - showInDialog: true, - }, includeDirectories: { type: 'array', label: 'Include Directories', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b10bbe1e7..685d818ca 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -575,7 +575,6 @@ export const AppContainer = (props: AppContainerProps) => { config.getExtensionContextFilePaths(), config.isTrustedFolder(), settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' - config.getFileFilteringOptions(), ); config.setUserMemory(memoryContent); diff --git a/packages/cli/src/ui/commands/directoryCommand.test.tsx b/packages/cli/src/ui/commands/directoryCommand.test.tsx index 850699551..54b6e0a3a 100644 --- a/packages/cli/src/ui/commands/directoryCommand.test.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.test.tsx @@ -54,9 +54,7 @@ describe('directoryCommand', () => { services: { config: mockConfig, settings: { - merged: { - memoryDiscoveryMaxDirs: 1000, - }, + merged: {}, }, }, ui: { diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 536cc9bbf..f5c91b46b 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -119,8 +119,6 @@ export const directoryCommand: SlashCommand = { config.getFolderTrust(), context.services.settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' - config.getFileFilteringOptions(), - context.services.settings.merged.context?.discoveryMaxDirs, ); config.setUserMemory(memoryContent); config.setGeminiMdFileCount(fileCount); diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts index 7e20bf11c..34df95d0d 100644 --- a/packages/cli/src/ui/commands/memoryCommand.test.ts +++ b/packages/cli/src/ui/commands/memoryCommand.test.ts @@ -299,9 +299,7 @@ describe('memoryCommand', () => { services: { config: mockConfig, settings: { - merged: { - memoryDiscoveryMaxDirs: 1000, - }, + merged: {}, } as LoadedSettings, }, }); diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index 05641178c..d9d2950b1 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -315,8 +315,6 @@ export const memoryCommand: SlashCommand = { config.getFolderTrust(), context.services.settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' - config.getFileFilteringOptions(), - context.services.settings.merged.context?.discoveryMaxDirs, ); config.setUserMemory(memoryContent); config.setGeminiMdFileCount(fileCount); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 9e4d294e0..1f20b40c0 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1331,9 +1331,7 @@ describe('SettingsDialog', () => { truncateToolOutputThreshold: 50000, truncateToolOutputLines: 1000, }, - context: { - discoveryMaxDirs: 500, - }, + context: {}, model: { maxSessionTurns: 100, skipNextSpeakerCheck: false, @@ -1466,7 +1464,6 @@ describe('SettingsDialog', () => { disableFuzzySearch: true, }, loadMemoryFromIncludeDirectories: true, - discoveryMaxDirs: 100, }, }); const onSelect = vi.fn(); diff --git a/packages/core/src/utils/bfsFileSearch.test.ts b/packages/core/src/utils/bfsFileSearch.test.ts deleted file mode 100644 index 292ec9d6f..000000000 --- a/packages/core/src/utils/bfsFileSearch.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import * as fsPromises from 'node:fs/promises'; -import * as path from 'node:path'; -import * as os from 'node:os'; -import { bfsFileSearch } from './bfsFileSearch.js'; -import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; - -describe('bfsFileSearch', () => { - let testRootDir: string; - - async function createEmptyDir(...pathSegments: string[]) { - const fullPath = path.join(testRootDir, ...pathSegments); - await fsPromises.mkdir(fullPath, { recursive: true }); - return fullPath; - } - - async function createTestFile(content: string, ...pathSegments: string[]) { - const fullPath = path.join(testRootDir, ...pathSegments); - await fsPromises.mkdir(path.dirname(fullPath), { recursive: true }); - await fsPromises.writeFile(fullPath, content); - return fullPath; - } - - beforeEach(async () => { - testRootDir = await fsPromises.mkdtemp( - path.join(os.tmpdir(), 'bfs-file-search-test-'), - ); - }); - - afterEach(async () => { - await fsPromises.rm(testRootDir, { recursive: true, force: true }); - }); - - it('should find a file in the root directory', async () => { - const targetFilePath = await createTestFile('content', 'target.txt'); - const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); - expect(result).toEqual([targetFilePath]); - }); - - it('should find a file in a nested directory', async () => { - const targetFilePath = await createTestFile( - 'content', - 'a', - 'b', - 'target.txt', - ); - const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); - expect(result).toEqual([targetFilePath]); - }); - - it('should find multiple files with the same name', async () => { - const targetFilePath1 = await createTestFile('content1', 'a', 'target.txt'); - const targetFilePath2 = await createTestFile('content2', 'b', 'target.txt'); - const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); - result.sort(); - expect(result).toEqual([targetFilePath1, targetFilePath2].sort()); - }); - - it('should return an empty array if no file is found', async () => { - await createTestFile('content', 'other.txt'); - const result = await bfsFileSearch(testRootDir, { fileName: 'target.txt' }); - expect(result).toEqual([]); - }); - - it('should ignore directories specified in ignoreDirs', async () => { - await createTestFile('content', 'ignored', 'target.txt'); - const targetFilePath = await createTestFile( - 'content', - 'not-ignored', - 'target.txt', - ); - const result = await bfsFileSearch(testRootDir, { - fileName: 'target.txt', - ignoreDirs: ['ignored'], - }); - expect(result).toEqual([targetFilePath]); - }); - - it('should respect the maxDirs limit and not find the file', async () => { - await createTestFile('content', 'a', 'b', 'c', 'target.txt'); - const result = await bfsFileSearch(testRootDir, { - fileName: 'target.txt', - maxDirs: 3, - }); - expect(result).toEqual([]); - }); - - it('should respect the maxDirs limit and find the file', async () => { - const targetFilePath = await createTestFile( - 'content', - 'a', - 'b', - 'c', - 'target.txt', - ); - const result = await bfsFileSearch(testRootDir, { - fileName: 'target.txt', - maxDirs: 4, - }); - expect(result).toEqual([targetFilePath]); - }); - - describe('with FileDiscoveryService', () => { - let projectRoot: string; - - beforeEach(async () => { - projectRoot = await createEmptyDir('project'); - }); - - it('should ignore gitignored files', async () => { - await createEmptyDir('project', '.git'); - await createTestFile('node_modules/', 'project', '.gitignore'); - await createTestFile('content', 'project', 'node_modules', 'target.txt'); - const targetFilePath = await createTestFile( - 'content', - 'project', - 'not-ignored', - 'target.txt', - ); - - const fileService = new FileDiscoveryService(projectRoot); - const result = await bfsFileSearch(projectRoot, { - fileName: 'target.txt', - fileService, - fileFilteringOptions: { - respectGitIgnore: true, - respectQwenIgnore: true, - }, - }); - - expect(result).toEqual([targetFilePath]); - }); - - it('should ignore qwenignored files', async () => { - await createTestFile('node_modules/', 'project', '.qwenignore'); - await createTestFile('content', 'project', 'node_modules', 'target.txt'); - const targetFilePath = await createTestFile( - 'content', - 'project', - 'not-ignored', - 'target.txt', - ); - - const fileService = new FileDiscoveryService(projectRoot); - const result = await bfsFileSearch(projectRoot, { - fileName: 'target.txt', - fileService, - fileFilteringOptions: { - respectGitIgnore: false, - respectQwenIgnore: true, - }, - }); - - expect(result).toEqual([targetFilePath]); - }); - - it('should not ignore files if respect flags are false', async () => { - await createEmptyDir('project', '.git'); - await createTestFile('node_modules/', 'project', '.gitignore'); - const target1 = await createTestFile( - 'content', - 'project', - 'node_modules', - 'target.txt', - ); - const target2 = await createTestFile( - 'content', - 'project', - 'not-ignored', - 'target.txt', - ); - - const fileService = new FileDiscoveryService(projectRoot); - const result = await bfsFileSearch(projectRoot, { - fileName: 'target.txt', - fileService, - fileFilteringOptions: { - respectGitIgnore: false, - respectQwenIgnore: false, - }, - }); - - expect(result.sort()).toEqual([target1, target2].sort()); - }); - }); - - it('should find all files in a complex directory structure', async () => { - // Create a complex directory structure to test correctness at scale - // without flaky performance checks. - const numDirs = 50; - const numFilesPerDir = 2; - const numTargetDirs = 10; - - const dirCreationPromises: Array> = []; - for (let i = 0; i < numDirs; i++) { - dirCreationPromises.push(createEmptyDir(`dir${i}`)); - dirCreationPromises.push(createEmptyDir(`dir${i}`, 'subdir1')); - dirCreationPromises.push(createEmptyDir(`dir${i}`, 'subdir2')); - dirCreationPromises.push(createEmptyDir(`dir${i}`, 'subdir1', 'deep')); - } - await Promise.all(dirCreationPromises); - - const fileCreationPromises: Array> = []; - for (let i = 0; i < numTargetDirs; i++) { - // Add target files in some directories - fileCreationPromises.push( - createTestFile('content', `dir${i}`, 'QWEN.md'), - ); - fileCreationPromises.push( - createTestFile('content', `dir${i}`, 'subdir1', 'QWEN.md'), - ); - } - const expectedFiles = await Promise.all(fileCreationPromises); - - const result = await bfsFileSearch(testRootDir, { - fileName: 'QWEN.md', - // Provide a generous maxDirs limit to ensure it doesn't prematurely stop - // in this large test case. Total dirs created is 200. - maxDirs: 250, - }); - - // Verify we found the exact files we created - expect(result.length).toBe(numTargetDirs * numFilesPerDir); - expect(result.sort()).toEqual(expectedFiles.sort()); - }); -}); diff --git a/packages/core/src/utils/bfsFileSearch.ts b/packages/core/src/utils/bfsFileSearch.ts deleted file mode 100644 index 755795980..000000000 --- a/packages/core/src/utils/bfsFileSearch.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; -import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; -import type { FileFilteringOptions } from '../config/constants.js'; -// Simple console logger for now. -// TODO: Integrate with a more robust server-side logger. -const logger = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - debug: (...args: any[]) => console.debug('[DEBUG] [BfsFileSearch]', ...args), -}; - -interface BfsFileSearchOptions { - fileName: string; - ignoreDirs?: string[]; - maxDirs?: number; - debug?: boolean; - fileService?: FileDiscoveryService; - fileFilteringOptions?: FileFilteringOptions; -} - -/** - * Performs a breadth-first search for a specific file within a directory structure. - * - * @param rootDir The directory to start the search from. - * @param options Configuration for the search. - * @returns A promise that resolves to an array of paths where the file was found. - */ -export async function bfsFileSearch( - rootDir: string, - options: BfsFileSearchOptions, -): Promise { - const { - fileName, - ignoreDirs = [], - maxDirs = Infinity, - debug = false, - fileService, - } = options; - const foundFiles: string[] = []; - const queue: string[] = [rootDir]; - const visited = new Set(); - let scannedDirCount = 0; - let queueHead = 0; // Pointer-based queue head to avoid expensive splice operations - - // Convert ignoreDirs array to Set for O(1) lookup performance - const ignoreDirsSet = new Set(ignoreDirs); - - // Process directories in parallel batches for maximum performance - const PARALLEL_BATCH_SIZE = 15; // Parallel processing batch size for optimal performance - - while (queueHead < queue.length && scannedDirCount < maxDirs) { - // Fill batch with unvisited directories up to the desired size - const batchSize = Math.min(PARALLEL_BATCH_SIZE, maxDirs - scannedDirCount); - const currentBatch = []; - while (currentBatch.length < batchSize && queueHead < queue.length) { - const currentDir = queue[queueHead]; - queueHead++; - if (!visited.has(currentDir)) { - visited.add(currentDir); - currentBatch.push(currentDir); - } - } - scannedDirCount += currentBatch.length; - - if (currentBatch.length === 0) continue; - - if (debug) { - logger.debug( - `Scanning [${scannedDirCount}/${maxDirs}]: batch of ${currentBatch.length}`, - ); - } - - // Read directories in parallel instead of one by one - const readPromises = currentBatch.map(async (currentDir) => { - try { - const entries = await fs.readdir(currentDir, { withFileTypes: true }); - return { currentDir, entries }; - } catch (error) { - // Warn user that a directory could not be read, as this affects search results. - const message = (error as Error)?.message ?? 'Unknown error'; - console.warn( - `[WARN] Skipping unreadable directory: ${currentDir} (${message})`, - ); - if (debug) { - logger.debug(`Full error for ${currentDir}:`, error); - } - return { currentDir, entries: [] }; - } - }); - - const results = await Promise.all(readPromises); - - for (const { currentDir, entries } of results) { - for (const entry of entries) { - const fullPath = path.join(currentDir, entry.name); - const isDirectory = entry.isDirectory(); - const isMatchingFile = entry.isFile() && entry.name === fileName; - - if (!isDirectory && !isMatchingFile) { - continue; - } - if (isDirectory && ignoreDirsSet.has(entry.name)) { - continue; - } - - if ( - fileService?.shouldIgnoreFile(fullPath, { - respectGitIgnore: options.fileFilteringOptions?.respectGitIgnore, - respectQwenIgnore: options.fileFilteringOptions?.respectQwenIgnore, - }) - ) { - continue; - } - - if (isDirectory) { - queue.push(fullPath); - } else { - foundFiles.push(fullPath); - } - } - } - } - - return foundFiles; -} diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index 1135d9ead..f66418cdc 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -209,7 +209,7 @@ describe('loadServerHierarchicalMemory', () => { }); }); - it('should load context files by downward traversal with custom filename', async () => { + it('should load context files from CWD with custom filename (not subdirectories)', async () => { const customFilename = 'LOCAL_CONTEXT.md'; setGeminiMdFilename(customFilename); @@ -228,9 +228,10 @@ describe('loadServerHierarchicalMemory', () => { DEFAULT_FOLDER_TRUST, ); + // Only upward traversal is performed, subdirectory files are not loaded expect(result).toEqual({ - memoryContent: `--- Context from: ${customFilename} ---\nCWD custom memory\n--- End of Context from: ${customFilename} ---\n\n--- Context from: ${path.join('subdir', customFilename)} ---\nSubdir custom memory\n--- End of Context from: ${path.join('subdir', customFilename)} ---`, - fileCount: 2, + memoryContent: `--- Context from: ${customFilename} ---\nCWD custom memory\n--- End of Context from: ${customFilename} ---`, + fileCount: 1, }); }); @@ -259,7 +260,7 @@ describe('loadServerHierarchicalMemory', () => { }); }); - it('should load ORIGINAL_GEMINI_MD_FILENAME files by downward traversal from CWD', async () => { + it('should only load context files from CWD, not subdirectories', async () => { await createTestFile( path.join(cwd, 'subdir', DEFAULT_CONTEXT_FILENAME), 'Subdir memory', @@ -278,13 +279,14 @@ describe('loadServerHierarchicalMemory', () => { DEFAULT_FOLDER_TRUST, ); + // Subdirectory files are not loaded, only CWD and upward expect(result).toEqual({ - memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---\nCWD memory\n--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---\n\n--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---\nSubdir memory\n--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`, - fileCount: 2, + memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---\nCWD memory\n--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---`, + fileCount: 1, }); }); - it('should load and correctly order global, upward, and downward ORIGINAL_GEMINI_MD_FILENAME files', async () => { + it('should load and correctly order global and upward context files', async () => { const defaultContextFile = await createTestFile( path.join(homedir, QWEN_DIR, DEFAULT_CONTEXT_FILENAME), 'default context content', @@ -301,7 +303,7 @@ describe('loadServerHierarchicalMemory', () => { path.join(cwd, DEFAULT_CONTEXT_FILENAME), 'CWD memory', ); - const subDirGeminiFile = await createTestFile( + await createTestFile( path.join(cwd, 'sub', DEFAULT_CONTEXT_FILENAME), 'Subdir memory', ); @@ -315,92 +317,10 @@ describe('loadServerHierarchicalMemory', () => { DEFAULT_FOLDER_TRUST, ); + // Subdirectory files are not loaded, only global and upward from CWD expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---\n\n--- Context from: ${path.relative(cwd, rootGeminiFile)} ---\nProject parent memory\n--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---\nCWD memory\n--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---\nSubdir memory\n--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`, - fileCount: 5, - }); - }); - - 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 = await loadServerHierarchicalMemory( - cwd, - [], - false, - new FileDiscoveryService(projectRoot), - [], - DEFAULT_FOLDER_TRUST, - 'tree', - { - respectGitIgnore: true, - respectQwenIgnore: true, - }, - 200, // maxDirs parameter - ); - - expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---\nMy code memory\n--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`, - fileCount: 1, - }); - }); - - it('should respect the maxDirs parameter during downward scan', async () => { - const consoleDebugSpy = vi - .spyOn(console, 'debug') - .mockImplementation(() => {}); - - // 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, - [], - true, - new FileDiscoveryService(projectRoot), - [], - DEFAULT_FOLDER_TRUST, - 'tree', // importFormat - { - respectGitIgnore: true, - respectQwenIgnore: true, - }, - 1, // maxDirs - ); - - expect(consoleDebugSpy).toHaveBeenCalledWith( - expect.stringContaining('[DEBUG] [BfsFileSearch]'), - expect.stringContaining('Scanning [1/1]:'), - ); - - vi.mocked(console.debug).mockRestore(); - - const result = await loadServerHierarchicalMemory( - cwd, - [], - false, - new FileDiscoveryService(projectRoot), - [], - DEFAULT_FOLDER_TRUST, - ); - - expect(result).toEqual({ - memoryContent: '', - fileCount: 0, + memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---\n\n--- Context from: ${path.relative(cwd, rootGeminiFile)} ---\nProject parent memory\n--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---\nCWD memory\n--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---`, + fileCount: 4, }); }); diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index 7c2a6c34c..ab45ef7e2 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -8,12 +8,9 @@ import * as fs from 'node:fs/promises'; import * as fsSync from 'node:fs'; import * as path from 'node:path'; import { homedir } from 'node:os'; -import { bfsFileSearch } from './bfsFileSearch.js'; import { getAllGeminiMdFilenames } from '../tools/memoryTool.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { processImports } from './memoryImportProcessor.js'; -import type { FileFilteringOptions } from '../config/constants.js'; -import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { QWEN_DIR } from './paths.js'; // Simple console logger, similar to the one previously in CLI's config.ts @@ -86,8 +83,6 @@ async function getGeminiMdFilePathsInternal( fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, - fileFilteringOptions: FileFilteringOptions, - maxDirs: number, ): Promise { const dirs = new Set([ ...includeDirectoriesToReadGemini, @@ -109,8 +104,6 @@ async function getGeminiMdFilePathsInternal( fileService, extensionContextFilePaths, folderTrust, - fileFilteringOptions, - maxDirs, ), ); @@ -139,8 +132,6 @@ async function getGeminiMdFilePathsInternalForEachDir( fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], folderTrust: boolean, - fileFilteringOptions: FileFilteringOptions, - maxDirs: number, ): Promise { const allPaths = new Set(); const geminiMdFilenames = getAllGeminiMdFilenames(); @@ -185,7 +176,7 @@ async function getGeminiMdFilePathsInternalForEachDir( // Not found, which is okay } } else if (dir && folderTrust) { - // FIX: Only perform the workspace search (upward and downward scans) + // FIX: Only perform the workspace search (upward scan from CWD to project root) // if a valid currentWorkingDirectory is provided and it's not the home directory. const resolvedCwd = path.resolve(dir); if (debugMode) @@ -225,23 +216,6 @@ async function getGeminiMdFilePathsInternalForEachDir( currentDir = path.dirname(currentDir); } upwardPaths.forEach((p) => allPaths.add(p)); - - const mergedOptions: FileFilteringOptions = { - ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, - ...fileFilteringOptions, - }; - - const downwardPaths = await bfsFileSearch(resolvedCwd, { - fileName: geminiMdFilename, - maxDirs, - debug: debugMode, - fileService, - fileFilteringOptions: mergedOptions, - }); - downwardPaths.sort(); - for (const dPath of downwardPaths) { - allPaths.add(dPath); - } } } @@ -364,8 +338,6 @@ export async function loadServerHierarchicalMemory( extensionContextFilePaths: string[] = [], folderTrust: boolean, importFormat: 'flat' | 'tree' = 'tree', - fileFilteringOptions?: FileFilteringOptions, - maxDirs: number = 200, ): Promise { if (debugMode) logger.debug( @@ -383,8 +355,6 @@ export async function loadServerHierarchicalMemory( fileService, extensionContextFilePaths, folderTrust, - fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, - maxDirs, ); if (filePaths.length === 0) { if (debugMode) logger.debug('No QWEN.md files found in hierarchy.'); @@ -400,6 +370,14 @@ export async function loadServerHierarchicalMemory( contentsWithPaths, currentWorkingDirectory, ); + + // Only count files that match configured memory filenames (e.g., QWEN.md), + // excluding system context files like output-language.md + const memoryFilenames = new Set(getAllGeminiMdFilenames()); + const fileCount = contentsWithPaths.filter((item) => + memoryFilenames.has(path.basename(item.filePath)), + ).length; + if (debugMode) logger.debug( `Combined instructions length: ${combinedInstructions.length}`, @@ -410,6 +388,6 @@ export async function loadServerHierarchicalMemory( ); return { memoryContent: combinedInstructions, - fileCount: contentsWithPaths.length, + fileCount, // Only count the context files }; }