11import { spawn } from "child_process" ;
2+ import { randomUUID } from "crypto" ;
3+ import * as fs from "fs" ;
4+ import * as os from "os" ;
5+ import * as path from "path" ;
26import { DEFAULT_BASH_TIMEOUT_MS , clampBashTimeoutMs } from "../common/bash-timeout" ;
37import { killProcessTree } from "../common/process-tree" ;
48import type { ProcessTimeoutControl , ProcessTimeoutInfo , ToolExecutionContext , ToolExecutionResult } from "./executor" ;
@@ -13,6 +17,7 @@ import {
1317
1418const MAX_OUTPUT_CHARS = 30000 ;
1519const MAX_CAPTURE_CHARS = 10 * 1024 * 1024 ;
20+ const BACKGROUND_OUTPUT_DIR = path . join ( os . tmpdir ( ) , "deepcode-background" ) ;
1621const sessionWorkingDirs = new Map < string , string > ( ) ;
1722
1823export function clearSessionWorkingDir ( sessionId : string ) : void {
@@ -51,6 +56,11 @@ export async function handleBashTool(
5156
5257 const startCwd = getSessionCwd ( context . sessionId , context . projectRoot ) ;
5358 const { shellPath, shellArgs, marker } = buildShellCommand ( command ) ;
59+ const runInBackground = isTrue ( args . run_in_background ) ;
60+
61+ if ( runInBackground ) {
62+ return startBackgroundShellCommand ( shellPath , shellArgs , startCwd , command , marker , context ) ;
63+ }
5464
5565 const execution = await executeShellCommand ( shellPath , shellArgs , startCwd , command , context ) ;
5666 const result = buildToolCommandResult (
@@ -75,6 +85,10 @@ export async function handleBashTool(
7585 return formatResult ( result , "bash" ) ;
7686}
7787
88+ function isTrue ( value : unknown ) : boolean {
89+ return value === true || value === "true" ;
90+ }
91+
7892function getSessionCwd ( sessionId : string , fallback : string ) : string {
7993 return sessionWorkingDirs . get ( sessionId ) ?? fallback ;
8094}
@@ -235,6 +249,125 @@ async function executeShellCommand(
235249 } ) ;
236250}
237251
252+ function startBackgroundShellCommand (
253+ shellPath : string ,
254+ shellArgs : string [ ] ,
255+ cwd : string ,
256+ command : string ,
257+ marker : string ,
258+ context : ToolExecutionContext
259+ ) : ToolExecutionResult {
260+ fs . mkdirSync ( BACKGROUND_OUTPUT_DIR , { recursive : true } ) ;
261+ const taskId = `bash-${ randomUUID ( ) } ` ;
262+ const outputPath = path . join ( BACKGROUND_OUTPUT_DIR , `${ taskId } .log` ) ;
263+ const startedAtMs = Date . now ( ) ;
264+ const detached = process . platform !== "win32" ;
265+ const configuredEnv = context . createOpenAIClient ?.( ) . env ?? { } ;
266+ const child = spawn ( shellPath , shellArgs , {
267+ cwd,
268+ env : buildShellEnv ( shellPath , configuredEnv ) ,
269+ detached,
270+ windowsHide : true ,
271+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
272+ } ) ;
273+ const pid = child . pid ;
274+ const processId = typeof pid === "number" ? pid : - 1 ;
275+
276+ let stdout = "" ;
277+ let stderr = "" ;
278+ let error : string | undefined ;
279+
280+ const appendOutputFile = ( chunk : string | Buffer ) => {
281+ try {
282+ fs . appendFileSync ( outputPath , chunk ) ;
283+ } catch {
284+ // Keep the background process running even if temp-file writes fail.
285+ }
286+ } ;
287+
288+ if ( typeof pid === "number" ) {
289+ context . onProcessStart ?.( pid , command ) ;
290+ }
291+
292+ child . stdout ?. on ( "data" , ( chunk : string | Buffer ) => {
293+ stdout = appendChunk ( stdout , chunk ) ;
294+ appendOutputFile ( chunk ) ;
295+ const text = typeof chunk === "string" ? chunk : chunk . toString ( "utf8" ) ;
296+ if ( typeof pid === "number" ) {
297+ context . onProcessStdout ?.( pid , text ) ;
298+ }
299+ } ) ;
300+ child . stderr ?. on ( "data" , ( chunk : string | Buffer ) => {
301+ stderr = appendChunk ( stderr , chunk ) ;
302+ appendOutputFile ( chunk ) ;
303+ const text = typeof chunk === "string" ? chunk : chunk . toString ( "utf8" ) ;
304+ if ( typeof pid === "number" ) {
305+ context . onProcessStdout ?.( pid , text ) ;
306+ }
307+ } ) ;
308+
309+ child . on ( "error" , ( spawnError ) => {
310+ error = spawnError . message ;
311+ } ) ;
312+
313+ child . on ( "close" , ( code , signal ) => {
314+ const markerResult = stripMarker ( stdout , marker ) ;
315+ const finalOutput = joinOutput ( markerResult . output , stderr ) ;
316+ const result = buildToolCommandResult (
317+ stdout ,
318+ stderr ,
319+ marker ,
320+ typeof code === "number" ? code : null ,
321+ signal ?? null ,
322+ shellPath ,
323+ cwd
324+ ) ;
325+ updateSessionCwd ( context . sessionId , cwd , result . cwd ) ;
326+ writeFinalBackgroundOutput ( outputPath , finalOutput ) ;
327+ if ( typeof pid === "number" ) {
328+ context . onProcessExit ?.( pid ) ;
329+ }
330+ const ok = ! error && result . exitCode === 0 && result . signal === null ;
331+ context . onBackgroundProcessComplete ?.( {
332+ taskId,
333+ processId,
334+ command,
335+ outputPath,
336+ ok,
337+ exitCode : result . exitCode ,
338+ signal : result . signal ,
339+ error : ok ? undefined : buildErrorMessage ( result . exitCode , result . signal , error ) ,
340+ cwd : result . cwd ,
341+ shellPath,
342+ startedAtMs,
343+ completedAtMs : Date . now ( ) ,
344+ } ) ;
345+ } ) ;
346+
347+ return {
348+ ok : true ,
349+ name : "bash" ,
350+ output : `Command running in background with ID: ${ taskId } . Output is being written to: ${ outputPath } ` ,
351+ metadata : {
352+ backgroundTaskId : taskId ,
353+ processId : typeof pid === "number" ? pid : null ,
354+ outputPath,
355+ cwd,
356+ shellPath,
357+ startCwd : cwd ,
358+ runInBackground : true ,
359+ } ,
360+ } ;
361+ }
362+
363+ function writeFinalBackgroundOutput ( outputPath : string , output : string | undefined ) : void {
364+ try {
365+ fs . writeFileSync ( outputPath , output ?? "" , "utf8" ) ;
366+ } catch {
367+ // Ignore notification/output persistence failures; the tool result already returned.
368+ }
369+ }
370+
238371function appendChunk ( existing : string , chunk : string | Buffer ) : string {
239372 if ( existing . length >= MAX_CAPTURE_CHARS ) {
240373 return existing ;
0 commit comments