@@ -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 ,
@@ -139,77 +141,87 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
139141 const stderrReader = createInterface ( { input : childProcess . child . stderr ! } ) ;
140142
141143 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- }
154-
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- }
164-
144+ if ( ! resolved ) {
145+ // Always collect lines, even after truncation is triggered
146+ // This allows us to save the full output to a temp file
165147 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 ( ) ;
148+
149+ if ( ! truncated ) {
150+ const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
151+
152+ // Check if line exceeds per-line limit
153+ if ( lineBytes > BASH_MAX_LINE_BYTES ) {
154+ truncated = true ;
155+ // Close readline interfaces before killing to ensure clean shutdown
156+ stdoutReader . close ( ) ;
157+ stderrReader . close ( ) ;
158+ childProcess . child . kill ( ) ;
159+ return ;
160+ }
161+
162+ totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
163+
164+ // Check if adding this line would exceed total bytes limit
165+ if ( totalBytesAccumulated > BASH_MAX_TOTAL_BYTES ) {
166+ truncated = true ;
167+ // Close readline interfaces before killing to ensure clean shutdown
168+ stdoutReader . close ( ) ;
169+ stderrReader . close ( ) ;
170+ childProcess . child . kill ( ) ;
171+ return ;
172+ }
173+
174+ // Check if we've exceeded the effective max_lines limit
175+ if ( lines . length >= effectiveMaxLines ) {
176+ truncated = true ;
177+ // Close readline interfaces before killing to ensure clean shutdown
178+ stdoutReader . close ( ) ;
179+ stderrReader . close ( ) ;
180+ childProcess . child . kill ( ) ;
181+ }
175182 }
176183 }
177184 } ) ;
178185
179186 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- }
192-
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- }
202-
187+ if ( ! resolved ) {
188+ // Always collect lines, even after truncation is triggered
189+ // This allows us to save the full output to a temp file
203190 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 ( ) ;
191+
192+ if ( ! truncated ) {
193+ const lineBytes = Buffer . byteLength ( line , "utf-8" ) ;
194+
195+ // Check if line exceeds per-line limit
196+ if ( lineBytes > BASH_MAX_LINE_BYTES ) {
197+ truncated = true ;
198+ // Close readline interfaces before killing to ensure clean shutdown
199+ stdoutReader . close ( ) ;
200+ stderrReader . close ( ) ;
201+ childProcess . child . kill ( ) ;
202+ return ;
203+ }
204+
205+ totalBytesAccumulated += lineBytes + 1 ; // +1 for newline
206+
207+ // Check if adding this line would exceed total bytes limit
208+ if ( totalBytesAccumulated > BASH_MAX_TOTAL_BYTES ) {
209+ truncated = true ;
210+ // Close readline interfaces before killing to ensure clean shutdown
211+ stdoutReader . close ( ) ;
212+ stderrReader . close ( ) ;
213+ childProcess . child . kill ( ) ;
214+ return ;
215+ }
216+
217+ // Check if we've exceeded the effective max_lines limit
218+ if ( lines . length >= effectiveMaxLines ) {
219+ truncated = true ;
220+ // Close readline interfaces before killing to ensure clean shutdown
221+ stdoutReader . close ( ) ;
222+ stderrReader . close ( ) ;
223+ childProcess . child . kill ( ) ;
224+ }
213225 }
214226 }
215227 } ) ;
@@ -294,15 +306,45 @@ export const createBashTool: ToolFactory = (config: ToolConfiguration) => {
294306 wall_duration_ms,
295307 } ) ;
296308 } 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- } ) ;
309+ // Save overflow output to temp file instead of returning an error
310+ // We don't show ANY of the actual output to avoid overwhelming context.
311+ // Instead, save it to a temp file and encourage the agent to use filtering tools.
312+ try {
313+ const tmpDir = os . tmpdir ( ) ;
314+ // Use 8 hex characters for short, memorable temp file IDs
315+ const fileId = Math . random ( ) . toString ( 16 ) . substring ( 2 , 10 ) ;
316+ const overflowPath = path . join ( tmpDir , `bash-${ fileId } .txt` ) ;
317+ const fullOutput = lines . join ( "\n" ) ;
318+ fs . writeFileSync ( overflowPath , fullOutput , "utf-8" ) ;
319+
320+ const output = `[OUTPUT OVERFLOW - ${ lines . length } lines saved to ${ overflowPath } ]
321+
322+ The command output exceeded limits and was saved to a temporary file.
323+ Use filtering tools to extract what you need:
324+ - grep '<pattern>' ${ overflowPath }
325+ - head -n 100 ${ overflowPath }
326+ - tail -n 100 ${ overflowPath }
327+ - sed -n '100,200p' ${ overflowPath }
328+
329+ When done, clean up: rm ${ overflowPath } ` ;
330+
331+ resolveOnce ( {
332+ success : false ,
333+ error : output ,
334+ exitCode : - 1 ,
335+ wall_duration_ms,
336+ } ) ;
337+ } catch ( err ) {
338+ // If temp file creation fails, fall back to original error
339+ resolveOnce ( {
340+ success : false ,
341+ error :
342+ `Command output exceeded limits (max ${ BASH_MAX_TOTAL_BYTES } bytes total, ${ BASH_MAX_LINE_BYTES } bytes per line, ${ effectiveMaxLines } lines). ` +
343+ `Use output-limiting commands like 'head', 'tail', or 'grep' to reduce output size. Failed to save overflow: ${ err } ` ,
344+ exitCode : - 1 ,
345+ wall_duration_ms,
346+ } ) ;
347+ }
306348 } else if ( exitCode === 0 || exitCode === null ) {
307349 resolveOnce ( {
308350 success : true ,
0 commit comments