Skip to content

Commit b9a2eb8

Browse files
committed
🤖 feat: Save overflow output to temp file in bash tool
When bash tool output exceeds max_lines limit, instead of discarding all output, save it to a temporary file and include the path in the truncated output. This prevents expensive commands from needing to be re-run just to see their full output. Changes: - Collect all output lines even after truncation is triggered - Write full output to /tmp/bash-overflow-<timestamp>.txt - Include file path in truncation marker - Update tests to verify overflow file creation and cleanup _Generated with `cmux`_
1 parent 56b251c commit b9a2eb8

File tree

2 files changed

+73
-13
lines changed

2 files changed

+73
-13
lines changed

src/services/tools/bash.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,54 @@ describe("bash tool", () => {
6060
expect(result.success).toBe(true);
6161
if (result.success) {
6262
const lines = result.output.split("\n");
63-
// Should have 5 lines plus the truncation marker on the last line
64-
expect(lines.length).toBe(5);
65-
expect(result.output).toContain("[TRUNCATED]");
63+
// Should have 5 lines plus the truncation marker line
64+
expect(lines.length).toBe(6); // 5 lines + truncation marker
65+
expect(result.output).toContain("[TRUNCATED");
6666
expect(result.truncated).toBe(true);
6767
}
6868
});
69+
70+
it("should save overflow output to temp file when truncated", async () => {
71+
const tool = createBashTool({ cwd: process.cwd() });
72+
const args: BashToolArgs = {
73+
script: "for i in {1..10}; do echo line$i; done",
74+
timeout_secs: 5,
75+
max_lines: 5,
76+
};
77+
78+
const result = (await tool.execute!(args, mockToolCallOptions)) as BashToolResult;
79+
80+
expect(result.success).toBe(true);
81+
if (result.success) {
82+
// Output should contain truncation marker with file path
83+
expect(result.output).toContain("[TRUNCATED - Full output");
84+
expect(result.output).toContain("lines) saved to:");
85+
expect(result.output).toContain("bash-overflow-");
86+
expect(result.truncated).toBe(true);
87+
88+
// Extract file path from output
89+
const match = result.output.match(/saved to: (.+)\]/);
90+
expect(match).toBeDefined();
91+
if (match) {
92+
const overflowPath = match[1];
93+
// Verify file exists
94+
const fs = require("fs");
95+
expect(fs.existsSync(overflowPath)).toBe(true);
96+
97+
// Verify file contains all 10 lines
98+
const fileContent = fs.readFileSync(overflowPath, "utf-8");
99+
const fileLines = fileContent.split("\n").filter((l: string) => l.length > 0);
100+
expect(fileLines.length).toBe(10);
101+
expect(fileContent).toContain("line1");
102+
expect(fileContent).toContain("line10");
103+
104+
// Clean up temp file
105+
fs.unlinkSync(overflowPath);
106+
}
107+
}
108+
});
109+
110+
69111
it("should clamp max_lines requests above the hard cap", async () => {
70112
const tool = createBashTool({ cwd: process.cwd() });
71113
const args: BashToolArgs = {
@@ -80,8 +122,8 @@ describe("bash tool", () => {
80122
if (result.success) {
81123
expect(result.truncated).toBe(true);
82124
const lines = result.output.split("\n");
83-
expect(lines).toHaveLength(BASH_HARD_MAX_LINES);
84-
expect(result.output).toContain("[TRUNCATED]");
125+
expect(lines).toHaveLength(BASH_HARD_MAX_LINES + 1); // +1 for truncation marker
126+
expect(result.output).toContain("[TRUNCATED");
85127
}
86128
});
87129

@@ -102,7 +144,7 @@ describe("bash tool", () => {
102144
expect(result.success).toBe(true);
103145
if (result.success) {
104146
expect(result.truncated).toBe(true);
105-
expect(result.output).toContain("[TRUNCATED]");
147+
expect(result.output).toContain("[TRUNCATED");
106148
// Should complete much faster than 10 seconds (give it 2 seconds buffer)
107149
expect(duration).toBeLessThan(2000);
108150
}

src/services/tools/bash.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { spawn } from "child_process";
33
import type { ChildProcess } from "child_process";
44
import { createInterface } from "readline";
55
import * as path from "path";
6+
import * as fs from "fs";
7+
import * as os from "os";
68
import { BASH_DEFAULT_MAX_LINES, BASH_HARD_MAX_LINES } from "@/constants/toolLimits";
79

810
import type { BashToolResult } from "@/types/tools";
@@ -131,10 +133,10 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
131133
const stderrReader = createInterface({ input: childProcess.child.stderr! });
132134

133135
stdoutReader.on("line", (line) => {
134-
if (!truncated && !resolved) {
136+
if (!resolved) {
135137
lines.push(line);
136138
// Check if we've exceeded the effective max_lines limit
137-
if (lines.length >= effectiveMaxLines) {
139+
if (!truncated && lines.length >= effectiveMaxLines) {
138140
truncated = true;
139141
// Close readline interfaces before killing to ensure clean shutdown
140142
stdoutReader.close();
@@ -145,10 +147,10 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
145147
});
146148

147149
stderrReader.on("line", (line) => {
148-
if (!truncated && !resolved) {
150+
if (!resolved) {
149151
lines.push(line);
150152
// Check if we've exceeded the effective max_lines limit
151-
if (lines.length >= effectiveMaxLines) {
153+
if (!truncated && lines.length >= effectiveMaxLines) {
152154
truncated = true;
153155
// Close readline interfaces before killing to ensure clean shutdown
154156
stdoutReader.close();
@@ -218,10 +220,26 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
218220
stdoutReader.close();
219221
stderrReader.close();
220222

221-
// Join lines and add truncation marker if needed
223+
// Join lines and handle truncation
222224
let output = lines.join("\n");
223-
if (truncated && output.length > 0) {
224-
output += " [TRUNCATED]";
225+
let overflowPath: string | undefined;
226+
227+
if (truncated && lines.length > 0) {
228+
// Write full output to temp file
229+
try {
230+
const tmpDir = os.tmpdir();
231+
const timestamp = Date.now();
232+
overflowPath = path.join(tmpDir, `bash-overflow-${timestamp}.txt`);
233+
fs.writeFileSync(overflowPath, output, "utf-8");
234+
235+
// Truncate output to max_lines and add marker with file path
236+
output = lines.slice(0, effectiveMaxLines).join("\n");
237+
output += `\n[TRUNCATED - Full output (${lines.length} lines) saved to: ${overflowPath}]`;
238+
} catch (err) {
239+
// If temp file creation fails, fall back to simple truncation
240+
output = lines.slice(0, effectiveMaxLines).join("\n");
241+
output += ` [TRUNCATED - ${lines.length} lines]`;
242+
}
225243
}
226244

227245
// Check if this was aborted (stream cancelled)

0 commit comments

Comments
 (0)