@@ -3,6 +3,8 @@ import { spawn } from "child_process";
33import type { ChildProcess } from "child_process" ;
44import { createInterface } from "readline" ;
55import * as path from "path" ;
6+ import * as fs from "fs" ;
7+ import * as os from "os" ;
68import {
79 BASH_DEFAULT_MAX_LINES ,
810 BASH_HARD_MAX_LINES ,
@@ -138,78 +140,73 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
138140 const stdoutReader = createInterface ( { input : childProcess . child . stdout ! } ) ;
139141 const stderrReader = createInterface ( { input : childProcess . child . stderr ! } ) ;
140142
143+ // Helper to trigger truncation and clean shutdown
144+ // Prevents duplication and ensures consistent cleanup
145+ const triggerTruncation = ( ) => {
146+ truncated = true ;
147+ stdoutReader . close ( ) ;
148+ stderrReader . close ( ) ;
149+ childProcess . child . kill ( ) ;
150+ } ;
151+
141152 stdoutReader . on ( "line" , ( line ) => {
142- if ( ! truncated && ! resolved ) {
143- const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
144-
145- // Check if line exceeds per-line limit
146- if ( lineBytes > BASH_MAX_LINE_BYTES ) {
147- truncated = true ;
148- // Close readline interfaces before killing to ensure clean shutdown
149- stdoutReader . close ( ) ;
150- stderrReader . close ( ) ;
151- childProcess . child . kill ( ) ;
152- return ;
153- }
153+ if ( ! resolved ) {
154+ // Always collect lines, even after truncation is triggered
155+ // This allows us to save the full output to a temp file
156+ lines . push ( line ) ;
154157
155- // Check if adding this line would exceed total bytes limit
156- if ( totalBytesAccumulated + lineBytes > BASH_MAX_TOTAL_BYTES ) {
157- truncated = true ;
158- // Close readline interfaces before killing to ensure clean shutdown
159- stdoutReader . close ( ) ;
160- stderrReader . close ( ) ;
161- childProcess . child . kill ( ) ;
162- return ;
163- }
158+ if ( ! truncated ) {
159+ const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
164160
165- lines . push ( line ) ;
166- totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
167-
168- // Check if we've exceeded the effective max_lines limit
169- if ( lines . length >= effectiveMaxLines ) {
170- truncated = true ;
171- // Close readline interfaces before killing to ensure clean shutdown
172- stdoutReader . close ( ) ;
173- stderrReader . close ( ) ;
174- childProcess . child . kill ( ) ;
161+ // Check if line exceeds per-line limit
162+ if ( lineBytes > BASH_MAX_LINE_BYTES ) {
163+ triggerTruncation ( ) ;
164+ return ;
165+ }
166+
167+ totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
168+
169+ // Check if adding this line would exceed total bytes limit
170+ if ( totalBytesAccumulated > BASH_MAX_TOTAL_BYTES ) {
171+ triggerTruncation ( ) ;
172+ return ;
173+ }
174+
175+ // Check if we've exceeded the effective max_lines limit
176+ if ( lines . length >= effectiveMaxLines ) {
177+ triggerTruncation ( ) ;
178+ }
175179 }
176180 }
177181 } ) ;
178182
179183 stderrReader . on ( "line" , ( line ) => {
180- if ( ! truncated && ! resolved ) {
181- const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
182-
183- // Check if line exceeds per-line limit
184- if ( lineBytes > BASH_MAX_LINE_BYTES ) {
185- truncated = true ;
186- // Close readline interfaces before killing to ensure clean shutdown
187- stdoutReader . close ( ) ;
188- stderrReader . close ( ) ;
189- childProcess . child . kill ( ) ;
190- return ;
191- }
184+ if ( ! resolved ) {
185+ // Always collect lines, even after truncation is triggered
186+ // This allows us to save the full output to a temp file
187+ lines . push ( line ) ;
192188
193- // Check if adding this line would exceed total bytes limit
194- if ( totalBytesAccumulated + lineBytes > BASH_MAX_TOTAL_BYTES ) {
195- truncated = true ;
196- // Close readline interfaces before killing to ensure clean shutdown
197- stdoutReader . close ( ) ;
198- stderrReader . close ( ) ;
199- childProcess . child . kill ( ) ;
200- return ;
201- }
189+ if ( ! truncated ) {
190+ const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
202191
203- lines . push ( line ) ;
204- totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
205-
206- // Check if we've exceeded the effective max_lines limit
207- if ( lines . length >= effectiveMaxLines ) {
208- truncated = true ;
209- // Close readline interfaces before killing to ensure clean shutdown
210- stdoutReader . close ( ) ;
211- stderrReader . close ( ) ;
212- childProcess . child . kill ( ) ;
192+ // Check if line exceeds per-line limit
193+ if ( lineBytes > BASH_MAX_LINE_BYTES ) {
194+ triggerTruncation ( ) ;
195+ return ;
196+ }
197+
198+ totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
199+
200+ // Check if adding this line would exceed total bytes limit
201+ if ( totalBytesAccumulated > BASH_MAX_TOTAL_BYTES ) {
202+ triggerTruncation ( ) ;
203+ return ;
204+ }
205+
206+ // Check if we've exceeded the effective max_lines limit
207+ if ( lines . length >= effectiveMaxLines ) {
208+ triggerTruncation ( ) ;
209+ }
213210 }
214211 }
215212 } ) ;
@@ -294,15 +291,45 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
294291 wall_duration_ms,
295292 } ) ;
296293 } else if ( truncated ) {
297- // Return error when output limits exceeded - no partial output
298- resolveOnce ( {
299- success : false ,
300- error :
301- `Command output exceeded limits (max ${ BASH_MAX_TOTAL_BYTES } bytes total, ${ BASH_MAX_LINE_BYTES } bytes per line, ${ effectiveMaxLines } lines). ` +
302- "Use output-limiting commands like 'head', 'tail', or 'grep' to reduce output size." ,
303- exitCode : - 1 ,
304- wall_duration_ms,
305- } ) ;
294+ // Save overflow output to temp file instead of returning an error
295+ // We don't show ANY of the actual output to avoid overwhelming context.
296+ // Instead, save it to a temp file and encourage the agent to use filtering tools.
297+ try {
298+ const tmpDir = os . tmpdir ( ) ;
299+ // Use 8 hex characters for short, memorable temp file IDs
300+ const fileId = Math . random ( ) . toString ( 16 ) . substring ( 2 , 10 ) ;
301+ const overflowPath = path . join ( tmpDir , `bash-${ fileId } .txt` ) ;
302+ const fullOutput = lines . join ( "\n" ) ;
303+ fs . writeFileSync ( overflowPath , fullOutput , "utf-8" ) ;
304+
305+ const output = `[OUTPUT OVERFLOW - ${ lines . length } lines saved to ${ overflowPath } ]
306+
307+ The command output exceeded limits and was saved to a temporary file.
308+ Use filtering tools to extract what you need:
309+ - grep '<pattern>' ${ overflowPath }
310+ - head -n 300 ${ overflowPath }
311+ - tail -n 300 ${ overflowPath }
312+ - sed -n '100,400p' ${ overflowPath }
313+
314+ When done, clean up: rm ${ overflowPath } ` ;
315+
316+ resolveOnce ( {
317+ success : false ,
318+ error : output ,
319+ exitCode : - 1 ,
320+ wall_duration_ms,
321+ } ) ;
322+ } catch ( err ) {
323+ // If temp file creation fails, fall back to original error
324+ resolveOnce ( {
325+ success : false ,
326+ error :
327+ `Command output exceeded limits (max ${ BASH_MAX_TOTAL_BYTES } bytes total, ${ BASH_MAX_LINE_BYTES } bytes per line, ${ effectiveMaxLines } lines). ` +
328+ `Use output-limiting commands like 'head', 'tail', or 'grep' to reduce output size. Failed to save overflow: ${ String ( err ) } ` ,
329+ exitCode : - 1 ,
330+ wall_duration_ms,
331+ } ) ;
332+ }
306333 } else if ( exitCode === 0 || exitCode === null ) {
307334 resolveOnce ( {
308335 success : true ,
0 commit comments