From 94fce4c3a6e5ebee828694f8fdf521c083f9de50 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 16 Aug 2026 08:16:53 +0800 Subject: [PATCH 1/2] fix: resolve local process wait after child close --- .changeset/fix-local-process-wait.md | 5 +++++ packages/ai-sandbox-local-process/src/handle.ts | 13 ++++++++----- .../tests/local-process.test.ts | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-local-process-wait.md diff --git a/.changeset/fix-local-process-wait.md b/.changeset/fix-local-process-wait.md new file mode 100644 index 0000000000..4c2bfec80c --- /dev/null +++ b/.changeset/fix-local-process-wait.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-sandbox-local-process': patch +--- + +Fix local-process spawn handles hanging when the child exits before `wait()` is called. diff --git a/packages/ai-sandbox-local-process/src/handle.ts b/packages/ai-sandbox-local-process/src/handle.ts index 28aa873bb9..a48c5907ce 100644 --- a/packages/ai-sandbox-local-process/src/handle.ts +++ b/packages/ai-sandbox-local-process/src/handle.ts @@ -340,6 +340,13 @@ export class LocalProcessHandle implements SandboxHandle { once: true, }) } + const closed = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code) => resolve(code ?? 0)) + }) + // The child can close while stdout is still being drained. Keep the + // rejection handled until the caller asks for the result via wait(). + closed.catch(() => {}) const handle: SpawnHandle = { pid: child.pid ?? -1, stdout: decodeStream(child.stdout), @@ -354,11 +361,7 @@ export class LocalProcessHandle implements SandboxHandle { child.stdin.end(() => resolve()) }), }, - wait: () => - new Promise((resolve, reject) => { - child.on('error', reject) - child.on('close', (code) => resolve(code ?? 0)) - }), + wait: () => closed, kill: (signal) => { killTree(child, signal) return Promise.resolve() diff --git a/packages/ai-sandbox-local-process/tests/local-process.test.ts b/packages/ai-sandbox-local-process/tests/local-process.test.ts index af949c7933..9501019918 100644 --- a/packages/ai-sandbox-local-process/tests/local-process.test.ts +++ b/packages/ai-sandbox-local-process/tests/local-process.test.ts @@ -88,6 +88,21 @@ describe('local-process process', () => { expect(code).toBe(0) await sbx.destroy() }) + + it('resolves wait() when the child closes before stdout is fully consumed', async () => { + const sbx = await fresh() + const proc = await sbx.process.spawn( + `node -e "for (let i = 0; i < 20; i++) console.log(i)"`, + ) + let out = '' + for await (const chunk of proc.stdout) { + out += chunk + await new Promise((resolve) => setTimeout(resolve, 10)) + } + await expect(proc.wait()).resolves.toBe(0) + expect(out.split('\n').filter(Boolean)).toHaveLength(20) + await sbx.destroy() + }) }) describe('local-process + spawnNdjson (real agent-CLI streaming)', () => { From 9472ccba992a897a42eb2fbbbaac1cac3875f103 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 16 Aug 2026 08:50:01 +0800 Subject: [PATCH 2/2] test: assert local process output ordering --- .../ai-sandbox-local-process/tests/local-process.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ai-sandbox-local-process/tests/local-process.test.ts b/packages/ai-sandbox-local-process/tests/local-process.test.ts index 9501019918..b77586c261 100644 --- a/packages/ai-sandbox-local-process/tests/local-process.test.ts +++ b/packages/ai-sandbox-local-process/tests/local-process.test.ts @@ -100,7 +100,10 @@ describe('local-process process', () => { await new Promise((resolve) => setTimeout(resolve, 10)) } await expect(proc.wait()).resolves.toBe(0) - expect(out.split('\n').filter(Boolean)).toHaveLength(20) + const lines = out.trimEnd().split(/\r?\n/) + expect(lines).toEqual( + Array.from({ length: 20 }, (_, index) => String(index)), + ) await sbx.destroy() }) })