diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts
index 58f3cc495e..0f81653a4e 100644
--- a/src/core/tools/ReadFileTool.ts
+++ b/src/core/tools/ReadFileTool.ts
@@ -123,6 +123,18 @@ export class ReadFileTool extends BaseTool<"read_file"> {
return
}
+ // Enforce maxConcurrentFileReads limit
+ const { maxConcurrentFileReads = 5 } = (await task.providerRef.deref()?.getState()) ?? {}
+ if (fileEntries.length > maxConcurrentFileReads) {
+ task.consecutiveMistakeCount++
+ task.recordToolError("read_file")
+ const errorMsg = `Too many files requested. You attempted to read ${fileEntries.length} files, but the concurrent file reads limit is ${maxConcurrentFileReads}. Please read files in batches of ${maxConcurrentFileReads} or fewer.`
+ await task.say("error", errorMsg)
+ const errorResult = useNative ? `Error: ${errorMsg}` : `${errorMsg}`
+ pushToolResult(errorResult)
+ return
+ }
+
const supportsImages = modelInfo.supportsImages ?? false
const fileResults: FileResult[] = fileEntries.map((entry) => ({
diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts
index c98211f940..882c7b3ebb 100644
--- a/src/core/tools/__tests__/readFileTool.spec.ts
+++ b/src/core/tools/__tests__/readFileTool.spec.ts
@@ -1771,3 +1771,188 @@ describe("read_file tool with image support", () => {
})
})
})
+
+describe("read_file tool concurrent file reads limit", () => {
+ const mockedCountFileLines = vi.mocked(countFileLines)
+ const mockedIsBinaryFile = vi.mocked(isBinaryFile)
+ const mockedPathResolve = vi.mocked(path.resolve)
+
+ let mockCline: any
+ let mockProvider: any
+ let toolResult: ToolResponse | undefined
+
+ beforeEach(() => {
+ // Clear specific mocks
+ mockedCountFileLines.mockClear()
+ mockedIsBinaryFile.mockClear()
+ mockedPathResolve.mockClear()
+ addLineNumbersMock.mockClear()
+ toolResultMock.mockClear()
+
+ // Use shared mock setup function
+ const mocks = createMockCline()
+ mockCline = mocks.mockCline
+ mockProvider = mocks.mockProvider
+
+ // Disable image support for these tests
+ setImageSupport(mockCline, false)
+
+ mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`)
+ mockedIsBinaryFile.mockResolvedValue(false)
+ mockedCountFileLines.mockResolvedValue(10)
+
+ toolResult = undefined
+ })
+
+ async function executeReadFileToolWithLimit(
+ fileCount: number,
+ maxConcurrentFileReads: number,
+ ): Promise {
+ // Setup provider state with the specified limit
+ mockProvider.getState.mockResolvedValue({
+ maxReadFileLine: -1,
+ maxConcurrentFileReads,
+ maxImageFileSize: 20,
+ maxTotalImageSize: 20,
+ })
+
+ // Create args with the specified number of files
+ const files = Array.from({ length: fileCount }, (_, i) => `file${i + 1}.txt`)
+ const argsContent = files.join("")
+
+ const toolUse: ReadFileToolUse = {
+ type: "tool_use",
+ name: "read_file",
+ params: { args: argsContent },
+ partial: false,
+ }
+
+ // Configure mocks for successful file reads
+ mockReadFileWithTokenBudget.mockResolvedValue({
+ content: "test content",
+ tokenCount: 10,
+ lineCount: 1,
+ complete: true,
+ })
+
+ await readFileTool.handle(mockCline, toolUse, {
+ askApproval: mockCline.ask,
+ handleError: vi.fn(),
+ pushToolResult: (result: ToolResponse) => {
+ toolResult = result
+ },
+ removeClosingTag: (_: ToolParamName, content?: string) => content ?? "",
+ toolProtocol: "xml",
+ })
+
+ return toolResult
+ }
+
+ it("should reject when file count exceeds maxConcurrentFileReads", async () => {
+ // Try to read 6 files when limit is 5
+ const result = await executeReadFileToolWithLimit(6, 5)
+
+ // Verify error result
+ expect(result).toContain("Error: Too many files requested")
+ expect(result).toContain("You attempted to read 6 files")
+ expect(result).toContain("but the concurrent file reads limit is 5")
+ expect(result).toContain("Please read files in batches of 5 or fewer")
+
+ // Verify error tracking
+ expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Too many files requested"))
+ })
+
+ it("should allow reading files when count equals maxConcurrentFileReads", async () => {
+ // Try to read exactly 5 files when limit is 5
+ const result = await executeReadFileToolWithLimit(5, 5)
+
+ // Should not contain error
+ expect(result).not.toContain("Error: Too many files requested")
+
+ // Should contain file results
+ expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt")
+ })
+
+ it("should allow reading files when count is below maxConcurrentFileReads", async () => {
+ // Try to read 3 files when limit is 5
+ const result = await executeReadFileToolWithLimit(3, 5)
+
+ // Should not contain error
+ expect(result).not.toContain("Error: Too many files requested")
+
+ // Should contain file results
+ expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt")
+ })
+
+ it("should respect custom maxConcurrentFileReads value of 1", async () => {
+ // Try to read 2 files when limit is 1
+ const result = await executeReadFileToolWithLimit(2, 1)
+
+ // Verify error result with limit of 1
+ expect(result).toContain("Error: Too many files requested")
+ expect(result).toContain("You attempted to read 2 files")
+ expect(result).toContain("but the concurrent file reads limit is 1")
+ })
+
+ it("should allow single file read when maxConcurrentFileReads is 1", async () => {
+ // Try to read 1 file when limit is 1
+ const result = await executeReadFileToolWithLimit(1, 1)
+
+ // Should not contain error
+ expect(result).not.toContain("Error: Too many files requested")
+
+ // Should contain file result
+ expect(typeof result === "string" ? result : JSON.stringify(result)).toContain("file1.txt")
+ })
+
+ it("should respect higher maxConcurrentFileReads value", async () => {
+ // Try to read 15 files when limit is 10
+ const result = await executeReadFileToolWithLimit(15, 10)
+
+ // Verify error result
+ expect(result).toContain("Error: Too many files requested")
+ expect(result).toContain("You attempted to read 15 files")
+ expect(result).toContain("but the concurrent file reads limit is 10")
+ })
+
+ it("should use default value of 5 when maxConcurrentFileReads is not set", async () => {
+ // Setup provider state without maxConcurrentFileReads
+ mockProvider.getState.mockResolvedValue({
+ maxReadFileLine: -1,
+ maxImageFileSize: 20,
+ maxTotalImageSize: 20,
+ })
+
+ // Create args with 6 files
+ const files = Array.from({ length: 6 }, (_, i) => `file${i + 1}.txt`)
+ const argsContent = files.join("")
+
+ const toolUse: ReadFileToolUse = {
+ type: "tool_use",
+ name: "read_file",
+ params: { args: argsContent },
+ partial: false,
+ }
+
+ mockReadFileWithTokenBudget.mockResolvedValue({
+ content: "test content",
+ tokenCount: 10,
+ lineCount: 1,
+ complete: true,
+ })
+
+ await readFileTool.handle(mockCline, toolUse, {
+ askApproval: mockCline.ask,
+ handleError: vi.fn(),
+ pushToolResult: (result: ToolResponse) => {
+ toolResult = result
+ },
+ removeClosingTag: (_: ToolParamName, content?: string) => content ?? "",
+ toolProtocol: "xml",
+ })
+
+ // Should use default limit of 5 and reject 6 files
+ expect(toolResult).toContain("Error: Too many files requested")
+ expect(toolResult).toContain("but the concurrent file reads limit is 5")
+ })
+})