diff --git a/src/core/output/outputSort.ts b/src/core/output/outputSort.ts index 1cc9cdb07..42b84eab7 100644 --- a/src/core/output/outputSort.ts +++ b/src/core/output/outputSort.ts @@ -5,8 +5,10 @@ import { logger } from '../../shared/logger.js'; import type { ProcessedFile } from '../file/fileTypes.js'; import { getFileChangeCount, isGitInstalled } from '../git/gitRepositoryHandle.js'; -// Cache for git file change counts to avoid repeated git operations +// Cache for git file change counts to avoid repeated git operations. +// Capped to prevent unbounded growth in long-running MCP server processes. // Key format: `${cwd}:${maxCommits}` +const MAX_CACHE_SIZE = 50; const fileChangeCountsCache = new Map>(); // Cache for git availability check per cwd @@ -48,6 +50,12 @@ const getFileChangeCounts = async ( // Fetch from git log try { const fileChangeCounts = await deps.getFileChangeCount(cwd, maxCommits); + if (fileChangeCountsCache.size >= MAX_CACHE_SIZE) { + const oldestKey = fileChangeCountsCache.keys().next().value; + if (oldestKey !== undefined) { + fileChangeCountsCache.delete(oldestKey); + } + } fileChangeCountsCache.set(cacheKey, fileChangeCounts); logger.trace('Git File change counts max commits:', maxCommits); diff --git a/src/core/treeSitter/languageParser.ts b/src/core/treeSitter/languageParser.ts index f9cca5b42..cf0878cf8 100644 --- a/src/core/treeSitter/languageParser.ts +++ b/src/core/treeSitter/languageParser.ts @@ -107,8 +107,9 @@ export class LanguageParser { public async dispose(): Promise { for (const resources of this.loadedResources.values()) { + resources.query.delete(); resources.parser.delete(); - logger.debug(`Deleted parser for language: ${resources.lang}`); + logger.debug(`Deleted parser and query for language: ${resources.lang}`); } this.loadedResources.clear(); this.initialized = false; diff --git a/src/core/treeSitter/parseFile.ts b/src/core/treeSitter/parseFile.ts index e5968ecaf..df0e78c9a 100644 --- a/src/core/treeSitter/parseFile.ts +++ b/src/core/treeSitter/parseFile.ts @@ -55,14 +55,15 @@ export const parseFile = async (fileContent: string, filePath: string, config: R const processedChunks = new Set(); const capturedChunks: CapturedChunk[] = []; - try { - // Parse the file content into an Abstract Syntax Tree (AST) - const tree = parser.parse(fileContent); - if (!tree) { - logger.debug(`Failed to parse file: ${filePath}`); - return undefined; - } + // The tree allocates native WASM memory that is not managed by the JS garbage + // collector, so it must be explicitly freed via tree.delete(). + const tree = parser.parse(fileContent); + if (!tree) { + logger.debug(`Failed to parse file: ${filePath}`); + return undefined; + } + try { // Get the appropriate parse strategy for the language const parseStrategy = await languageParser.getStrategyForLang(lang); @@ -93,6 +94,8 @@ export const parseFile = async (fileContent: string, filePath: string, config: R } } catch (error: unknown) { logger.log(`Error parsing file: ${error}\n`); + } finally { + tree.delete(); } const filteredChunks = filterDuplicatedChunks(capturedChunks); diff --git a/src/mcp/tools/mcpToolRuntime.ts b/src/mcp/tools/mcpToolRuntime.ts index bec8138ce..aa9bc29b8 100644 --- a/src/mcp/tools/mcpToolRuntime.ts +++ b/src/mcp/tools/mcpToolRuntime.ts @@ -6,11 +6,19 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { generateTreeString } from '../../core/file/fileTreeGenerate.js'; import type { ProcessedFile } from '../../core/file/fileTypes.js'; -// Map to store generated output files +// Map to store generated output files. Capped to prevent unbounded growth +// in long-running MCP server processes. +const MAX_REGISTRY_SIZE = 100; const outputFileRegistry = new Map(); -// Register an output file +// Register an output file, evicting the oldest entry if at capacity export const registerOutputFile = (id: string, filePath: string): void => { + if (outputFileRegistry.size >= MAX_REGISTRY_SIZE) { + const oldestKey = outputFileRegistry.keys().next().value; + if (oldestKey !== undefined) { + outputFileRegistry.delete(oldestKey); + } + } outputFileRegistry.set(id, filePath); };