From f75fa665a6210d6cb984921954a67e4f91dcdfee Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 8 Oct 2025 20:36:54 -0500 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20Improve=20bash=20tool=20over?= =?UTF-8?q?flow=20error=20messages=20with=20specific=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, bash tool overflow errors gave generic messages that didn't help agents understand what went wrong or how much data was processed. Changes: - Track specific overflow reason (line too long, total bytes exceeded, or max lines exceeded) - Include actual values in error messages (bytes read, line count, etc.) - Show which limit was violated with comparison (e.g., '17000 bytes > 16384 bytes') - Update overflow message format to include specific reason upfront - Improve fallback error when temp file creation fails Example new messages: - 'Line 50 exceeded per-line limit: 2048 bytes > 1024 bytes' - 'Total output exceeded limit: 17000 bytes > 16384 bytes (at line 200)' - 'Line count exceeded limit: 300 lines >= 300 lines (15000 bytes read)' This gives agents actionable information to adjust their commands (e.g., add grep filters, use head/tail with correct line counts). _Generated with `cmux`_ --- src/services/tools/bash.test.ts | 16 ++++++++------ src/services/tools/bash.ts | 37 ++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 601ab6dba..3f6013f9a 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -58,7 +58,8 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("output exceeded limits"); + // Should contain specific overflow reason + expect(result.error).toMatch(/Line count exceeded limit|OUTPUT OVERFLOW/); expect(result.exitCode).toBe(-1); } }); @@ -76,7 +77,10 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { expect(result.error).toContain("[OUTPUT OVERFLOW"); - expect(result.error).toContain("lines saved to"); + // Should contain specific overflow reason (one of the three types) + expect(result.error).toMatch(/Line count exceeded limit|Total output exceeded limit|exceeded per-line limit/); + expect(result.error).toContain("Full output"); + expect(result.error).toContain("lines) saved to"); expect(result.error).toContain("bash-"); expect(result.error).toContain(".txt"); @@ -130,7 +134,7 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("output exceeded limits"); + expect(result.error).toMatch(/Line count exceeded limit|OUTPUT OVERFLOW/); // Should complete much faster than 10 seconds (give it 2 seconds buffer) expect(duration).toBeLessThan(2000); } @@ -513,7 +517,7 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("output exceeded limits"); + expect(result.error).toMatch(/exceeded per-line limit|OUTPUT OVERFLOW/); expect(result.error).toContain("head"); expect(result.exitCode).toBe(-1); } @@ -533,7 +537,7 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("output exceeded limits"); + expect(result.error).toMatch(/Total output exceeded limit|OUTPUT OVERFLOW/); expect(result.error).toContain("grep"); expect(result.exitCode).toBe(-1); } @@ -551,7 +555,7 @@ describe("bash tool", () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.error).toContain("output exceeded limits"); + expect(result.error).toMatch(/Total output exceeded limit|OUTPUT OVERFLOW/); expect(result.error).toContain("tail"); expect(result.exitCode).toBe(-1); } diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index 4eeb9c349..6adc9f155 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -50,6 +50,7 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { const normalizedMaxLines = Math.max(1, Math.floor(max_lines)); const effectiveMaxLines = Math.min(normalizedMaxLines, BASH_HARD_MAX_LINES); let totalBytesAccumulated = 0; + let overflowReason: string | null = null; // Detect redundant cd to working directory // Match patterns like: "cd /path &&", "cd /path;", "cd '/path' &&", "cd \"/path\" &&" @@ -142,8 +143,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Helper to trigger truncation and clean shutdown // Prevents duplication and ensures consistent cleanup - const triggerTruncation = () => { + const triggerTruncation = (reason: string) => { truncated = true; + overflowReason = reason; stdoutReader.close(); stderrReader.close(); childProcess.child.kill(); @@ -160,7 +162,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Check if line exceeds per-line limit if (lineBytes > BASH_MAX_LINE_BYTES) { - triggerTruncation(); + triggerTruncation( + `Line ${lines.length} exceeded per-line limit: ${lineBytes} bytes > ${BASH_MAX_LINE_BYTES} bytes` + ); return; } @@ -168,13 +172,17 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Check if adding this line would exceed total bytes limit if (totalBytesAccumulated > BASH_MAX_TOTAL_BYTES) { - triggerTruncation(); + triggerTruncation( + `Total output exceeded limit: ${totalBytesAccumulated} bytes > ${BASH_MAX_TOTAL_BYTES} bytes (at line ${lines.length})` + ); return; } // Check if we've exceeded the effective max_lines limit if (lines.length >= effectiveMaxLines) { - triggerTruncation(); + triggerTruncation( + `Line count exceeded limit: ${lines.length} lines >= ${effectiveMaxLines} lines (${totalBytesAccumulated} bytes read)` + ); } } } @@ -191,7 +199,9 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Check if line exceeds per-line limit if (lineBytes > BASH_MAX_LINE_BYTES) { - triggerTruncation(); + triggerTruncation( + `Line ${lines.length} exceeded per-line limit: ${lineBytes} bytes > ${BASH_MAX_LINE_BYTES} bytes` + ); return; } @@ -199,13 +209,17 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { // Check if adding this line would exceed total bytes limit if (totalBytesAccumulated > BASH_MAX_TOTAL_BYTES) { - triggerTruncation(); + triggerTruncation( + `Total output exceeded limit: ${totalBytesAccumulated} bytes > ${BASH_MAX_TOTAL_BYTES} bytes (at line ${lines.length})` + ); return; } // Check if we've exceeded the effective max_lines limit if (lines.length >= effectiveMaxLines) { - triggerTruncation(); + triggerTruncation( + `Line count exceeded limit: ${lines.length} lines >= ${effectiveMaxLines} lines (${totalBytesAccumulated} bytes read)` + ); } } } @@ -302,9 +316,10 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { const fullOutput = lines.join("\n"); fs.writeFileSync(overflowPath, fullOutput, "utf-8"); - const output = `[OUTPUT OVERFLOW - ${lines.length} lines saved to ${overflowPath}] + const output = `[OUTPUT OVERFLOW - ${overflowReason}] + +Full output (${lines.length} lines) saved to ${overflowPath} -The command output exceeded limits and was saved to a temporary file. Use filtering tools to extract what you need: - grep '' ${overflowPath} - head -n 300 ${overflowPath} @@ -323,9 +338,7 @@ When done, clean up: rm ${overflowPath}`; // If temp file creation fails, fall back to original error resolveOnce({ success: false, - error: - `Command output exceeded limits (max ${BASH_MAX_TOTAL_BYTES} bytes total, ${BASH_MAX_LINE_BYTES} bytes per line, ${effectiveMaxLines} lines). ` + - `Use output-limiting commands like 'head', 'tail', or 'grep' to reduce output size. Failed to save overflow: ${String(err)}`, + error: `Command output overflow: ${overflowReason}. Failed to save overflow to temp file: ${String(err)}`, exitCode: -1, wall_duration_ms, }); From c5c5ace2c7c2ca8f560669ef72be6f54779ffc28 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 8 Oct 2025 20:39:30 -0500 Subject: [PATCH 2/4] Fix bash test regex to handle updated error message format The error message changed from "lines saved to" to "lines) saved to", breaking the regex that extracts the temp file path. Updated regex to be more flexible and match both formats. --- src/services/tools/bash.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 3f6013f9a..eab003dd4 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -90,8 +90,8 @@ describe("bash tool", () => { expect(result.error).toContain("tail -n 300"); expect(result.error).toContain("When done, clean up: rm"); - // Extract file path from error message - const match = /saved to (\/[^\]]+\.txt)/.exec(result.error); + // Extract file path from error message (handles both "lines saved to" and "lines) saved to") + const match = /saved to (\/.+?\.txt)/.exec(result.error); expect(match).toBeDefined(); if (match) { const overflowPath = match[1]; From 0c0ed5d28534be5cf7ff18d21a711d3571ef62fe Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 8 Oct 2025 20:42:40 -0500 Subject: [PATCH 3/4] Fix executeBash integration test assertions for new error format Updated assertions to check for new overflow error messages ("Line count exceeded limit" or "OUTPUT OVERFLOW") instead of the old generic "output exceeded limits" text. --- tests/ipcMain/executeBash.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ipcMain/executeBash.test.ts b/tests/ipcMain/executeBash.test.ts index ac2b39167..6fc651431 100644 --- a/tests/ipcMain/executeBash.test.ts +++ b/tests/ipcMain/executeBash.test.ts @@ -185,7 +185,7 @@ describeIntegration("IpcMain executeBash integration tests", () => { expect(maxLinesResult.success).toBe(true); expect(maxLinesResult.data.success).toBe(false); - expect(maxLinesResult.data.error).toContain("output exceeded limits"); + expect(maxLinesResult.data.error).toMatch(/Line count exceeded limit|OUTPUT OVERFLOW/); expect(maxLinesResult.data.exitCode).toBe(-1); // Clean up @@ -222,7 +222,7 @@ describeIntegration("IpcMain executeBash integration tests", () => { expect(oversizedResult.success).toBe(true); expect(oversizedResult.data.success).toBe(false); - expect(oversizedResult.data.error).toContain("output exceeded limits"); + expect(oversizedResult.data.error).toMatch(/Line count exceeded limit|OUTPUT OVERFLOW/); expect(oversizedResult.data.exitCode).toBe(-1); await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId); From 3feab22738a99d35703d33b22f6420710d57475f Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 8 Oct 2025 23:02:47 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20Validate=20bash=20tool=20scr?= =?UTF-8?q?ipt=20parameter=20and=20fix=20typecheck=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validation to reject empty or whitespace-only scripts - Fail immediately with clear error message for malformed tool calls - Add tests for empty script validation - Fix typecheck Makefile target to depend on version file generation --- Makefile | 2 +- src/services/tools/bash.test.ts | 38 +++++++++++++++++++++++++++++++++ src/services/tools/bash.ts | 10 +++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bba0c3f7d..0e18ae511 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ fmt-check: ## Check code formatting fmt-shell: ## Format shell scripts with shfmt @./scripts/fmt.sh --shell -typecheck: ## Run TypeScript type checking +typecheck: dist/version.txt ## Run TypeScript type checking @./scripts/typecheck.sh ## Testing diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index eab003dd4..5feb0e57f 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -560,4 +560,42 @@ describe("bash tool", () => { expect(result.exitCode).toBe(-1); } }); + + it("should fail immediately when script is empty", async () => { + const tool = createBashTool({ cwd: process.cwd() }); + const args: BashToolArgs = { + script: "", + timeout_secs: 5, + max_lines: 100, + }; + + const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult; + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("Script parameter is empty"); + expect(result.error).toContain("malformed tool call"); + expect(result.exitCode).toBe(-1); + expect(result.wall_duration_ms).toBe(0); + } + }); + + it("should fail immediately when script is only whitespace", async () => { + const tool = createBashTool({ cwd: process.cwd() }); + const args: BashToolArgs = { + script: " \n\t ", + timeout_secs: 5, + max_lines: 100, + }; + + const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult; + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain("Script parameter is empty"); + expect(result.exitCode).toBe(-1); + expect(result.wall_duration_ms).toBe(0); + } + }); + }); diff --git a/src/services/tools/bash.ts b/src/services/tools/bash.ts index 6adc9f155..20977ae8f 100644 --- a/src/services/tools/bash.ts +++ b/src/services/tools/bash.ts @@ -46,6 +46,16 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => { { script, timeout_secs, max_lines = BASH_DEFAULT_MAX_LINES, stdin }, { abortSignal } ): Promise => { + // Validate script is not empty - likely indicates a malformed tool call + if (!script || script.trim().length === 0) { + return { + success: false, + error: "Script parameter is empty. This likely indicates a malformed tool call.", + exitCode: -1, + wall_duration_ms: 0, + }; + } + const startTime = performance.now(); const normalizedMaxLines = Math.max(1, Math.floor(max_lines)); const effectiveMaxLines = Math.min(normalizedMaxLines, BASH_HARD_MAX_LINES);