The bash tool had two critical issues that caused tool call failures:
-
Backgrounded processes hang - Commands like
nohup python main.py &would cause the next 5-10 tool calls to return empty because Node'sexecwaited for stdio pipes to close, even though the process was backgrounded. -
Large output truncation - Commands producing >10MB output were silently killed with SIGTERM due to
maxBufferlimits, and stuck processes weren't properly terminated.
The bash tool used promisify(exec) which has two failure modes:
- maxBuffer limit: 10MB limit - when stdout exceeds this, exec kills the child with SIGTERM
- Stdio pipe blocking: Backgrounded processes inherit the shell's stdio handles. Node's exec waits for ALL stdio pipes to close before resolving, so even though
nohup ... &returned instantly, the tool stayed blocked on orphaned pipe references until the background process died or timeout fired (60s default).
Added isBackgroundedCommand() to detect commands ending with &, using nohup, or disown:
function isBackgroundedCommand(command: string): boolean {
const trimmed = command.trim();
return /&\s*$/.test(trimmed) || /\bnohup\b/.test(trimmed) || /\bdisown\b/.test(trimmed);
}Created executeBackgroundedCommand() that uses spawn with:
detached: true- process runs independentlystdio: 'ignore'- no stdio pipes inherited (prevents hanging)unref()- parent doesn't wait for child
const proc = spawn(shellPath, shellArgs, {
cwd: cwd || process.cwd(),
env: Object.keys(env).length > 0 ? { ...process.env, ...env } : process.env,
detached: true,
stdio: 'ignore', // Critical: don't inherit stdio pipes
});
proc.unref();Returns immediately with process PID and suggests using Job system for monitoring.
Raised maxBuffer from 10MB to 100MB:
maxBuffer: 100 * 1024 * 1024, // 100MB buffer (up from 10MB)Replaced SIGTERM with SIGKILL for stuck processes:
const killTimer = setTimeout(() => {
if (childProcess && !childProcess.killed) {
console.warn('[Bash Tool] Timeout exceeded, sending SIGKILL');
childProcess.kill('SIGKILL');
}
}, timeout + 5000); // Give exec 5s grace period, then SIGKILLApplied same fixes to executeBashCommandStreaming():
- SIGKILL instead of SIGTERM on timeout
- Proper cleanup of kill timer on process close/error
- Fixes hanging tool calls after backgrounded commands
- Prevents silent output truncation for high-volume commands
- Ensures processes actually die on timeout (SIGKILL vs SIGTERM)
- Returns immediately for backgrounded commands with PID info
-
Test backgrounded command:
nohup python -c "import time; time.sleep(100)" > /tmp/test.log 2>&1 &- Should return immediately with PID
- Next tool call should work instantly (not hang)
-
Test high-output command:
yes | head -n 1000000(generates ~10MB+ output)- Should succeed with 100MB buffer
- Previously would fail at 10MB
-
Test stuck process:
sleep 120with 5s timeout- Should be killed via SIGKILL after timeout
- Should return timeout error, not hang indefinitely
For long-running orchestration work (GCP deployments, test suites, training jobs), use the Job system instead of bash tool - Jobs handle their own stdio and are designed for long-running processes.