From 64413beb54dca557b5f573a416c7008a6d6d88aa Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 28 Sep 2026 14:00:15 -0700 Subject: [PATCH] Aesel 0.8.5: a command that starts a server no longer hangs the turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bash tool finished only when every holder of its output let go, so `python3 -m http.server … &` left a background subshell holding the pipe and the turn waited forever (a jumping-game session sat for minutes). It now returns when the shell exits, gives the output a moment to drain, and leaves background processes running. Tests cover both hang shapes. Co-Authored-By: Claude Opus 5.5 --- aesel/package.json | 2 +- aesel/src/workspace-tools.mjs | 15 +++++++++++++-- aesel/test/workspace-bash.test.mjs | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 aesel/test/workspace-bash.test.mjs diff --git a/aesel/package.json b/aesel/package.json index 177d94b7a8..261adfc699 100644 --- a/aesel/package.json +++ b/aesel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.8.4", + "version": "0.8.5", "private": true, "type": "module", "scripts": { diff --git a/aesel/src/workspace-tools.mjs b/aesel/src/workspace-tools.mjs index a1bfb2ebfe..2a21b6a47e 100644 --- a/aesel/src/workspace-tools.mjs +++ b/aesel/src/workspace-tools.mjs @@ -132,11 +132,22 @@ function run(command, args, { cwd, signal, timeout = COMMAND_TIMEOUT }) { clearTimeout(timer); done({ code: -1, output: output + error.message }); }); - child.on("close", (code) => { + // Done when the shell exits, not when every holder of its output lets go: + // a command that starts a server in the background (`… &`) leaves a child + // holding the pipe, and waiting for "close" waited forever. The output + // gets a moment to drain; the background process keeps running. + let finished = false; + const finish = (code) => { + if (finished) return; + finished = true; clearTimeout(timer); signal?.removeEventListener("abort", killGroup); + child.stdout.destroy(); + child.stderr.destroy(); done({ code: timedOut ? "timeout" : code, output }); - }); + }; + child.on("exit", (code) => setTimeout(() => finish(code), 150)); + child.on("close", (code) => finish(code)); }); } diff --git a/aesel/test/workspace-bash.test.mjs b/aesel/test/workspace-bash.test.mjs new file mode 100644 index 0000000000..f31d346d89 --- /dev/null +++ b/aesel/test/workspace-bash.test.mjs @@ -0,0 +1,18 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { runWorkspaceTool } from "../src/workspace-tools.mjs"; + +const approve = async () => true; + +test("a command that leaves a server running in the background still returns", async () => { + const started = Date.now(); + const out = await runWorkspaceTool("bash", { command: "echo up && (sleep 5 &) ; sleep 6 & echo done" }, { cwd: "/tmp", approve }); + assert.match(out, /up\ndone\n\[exit 0\]/); + assert.ok(Date.now() - started < 3000, "returned without waiting for the background sleeps"); +}); + +test("an interrupt kills the whole pipeline, not just the shell", async () => { + const started = Date.now(); + await runWorkspaceTool("bash", { command: "sleep 30 | cat" }, { cwd: "/tmp", approve, signal: AbortSignal.timeout(500) }); + assert.ok(Date.now() - started < 3000, "returned once interrupted"); +}); -- 2.51.2