Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/core/output/outputSort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, number>>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The gitAvailabilityCache (declared at line 15) is also a module-level Map that grows without limit as different directories are processed. To fully address the memory leak concerns in long-running processes, this cache should also be capped using a similar FIFO eviction strategy as fileChangeCountsCache. This is particularly relevant because cwd changes for every remote repository packing operation.


// Cache for git availability check per cwd
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/core/treeSitter/languageParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ export class LanguageParser {

public async dispose(): Promise<void> {
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;
Expand Down
17 changes: 10 additions & 7 deletions src/core/treeSitter/parseFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,15 @@ export const parseFile = async (fileContent: string, filePath: string, config: R
const processedChunks = new Set<string>();
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);

Expand Down Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions src/mcp/tools/mcpToolRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>();

// 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);
}
}
Comment on lines +16 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While evicting entries from outputFileRegistry prevents memory growth, the corresponding temporary files on disk are not deleted. In long-running scenarios with heavy usage, this could lead to significant disk space consumption in the system's temporary directory. Consider implementing a best-effort cleanup of the file when an entry is evicted (e.g., using fs.unlink). Note that registerOutputFile is currently synchronous, so this would need to be handled carefully to avoid blocking or complex async logic.

outputFileRegistry.set(id, filePath);
};

Expand Down
Loading