From 1bb4380aca42ee3a83200b1e4ba5a49c8e83ca49 Mon Sep 17 00:00:00 2001 From: dbpolito Date: Fri, 9 Jan 2026 15:03:44 -0300 Subject: [PATCH] fix(opencode): Improve killing bash tools on aborts --- packages/opencode/src/shell/shell.ts | 31 ++++++++++++++++++------ packages/opencode/test/tool/bash.test.ts | 27 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 2e8d48bfd92..6f508a34531 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -4,6 +4,7 @@ import path from "path" import { spawn, type ChildProcess } from "child_process" const SIGKILL_TIMEOUT_MS = 200 +const SIGINT_TIMEOUT_MS = 250 export namespace Shell { export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise { @@ -19,18 +20,32 @@ export namespace Shell { return } - try { + const exited = () => opts?.exited?.() === true + + const killGroup = async () => { + process.kill(-pid, "SIGINT") + await Bun.sleep(SIGINT_TIMEOUT_MS) + if (exited()) return process.kill(-pid, "SIGTERM") await Bun.sleep(SIGKILL_TIMEOUT_MS) - if (!opts?.exited?.()) { - process.kill(-pid, "SIGKILL") - } - } catch (_e) { + if (exited()) return + process.kill(-pid, "SIGKILL") + } + + const killSingle = async () => { + proc.kill("SIGINT") + await Bun.sleep(SIGINT_TIMEOUT_MS) + if (exited()) return proc.kill("SIGTERM") await Bun.sleep(SIGKILL_TIMEOUT_MS) - if (!opts?.exited?.()) { - proc.kill("SIGKILL") - } + if (exited()) return + proc.kill("SIGKILL") + } + + try { + await killGroup() + } catch (_e) { + await killSingle() } } const BLACKLIST = new Set(["fish", "nu"]) diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 750ff8193e9..bdbe0d520f8 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -36,6 +36,33 @@ describe("tool.bash", () => { }, }) }) + + test("abort sends SIGINT before SIGKILL", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const controller = new AbortController() + const bash = await BashTool.init() + const promise = bash.execute( + { + command: "trap 'echo INT_CAUGHT; exit 0' INT; while true; do sleep 0.05; done", + description: "Loop until interrupted", + timeout: 60_000, + }, + { + ...ctx, + abort: controller.signal, + }, + ) + + await Bun.sleep(150) + controller.abort() + + const result = await promise + expect(result.output).toContain("INT_CAUGHT") + }, + }) + }) }) describe("tool.bash permissions", () => {