From a1a1f568415d98ccedfa91e4a290cffa6ecb953e Mon Sep 17 00:00:00 2001 From: artale Date: Mon, 16 Feb 2026 14:00:45 +0100 Subject: [PATCH] fix(process): disable detached spawn on Windows to fix empty exec output (#18035) The supervisor's child adapter always spawned with `detached: true`, which creates a new process group. On Windows Scheduled Tasks (headless, no console), this prevents stdout/stderr pipes from properly connecting, causing all exec tool output to silently disappear. The old exec path (pre-supervisor refactor) never used `detached: true`. The regression was introduced in cd44a0d01 (refactor process spawning). Changes: - child.ts: set `detached: false` on Windows, keep `detached: true` on POSIX (where it's needed to survive parent exit). Skip the no-detach fallback on Windows since it's already the default. - child.test.ts: platform-aware assertions for detached behavior. Fixes #18035 Fixes #17806 --- src/process/supervisor/adapters/child.test.ts | 11 ++++++++-- src/process/supervisor/adapters/child.ts | 22 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/process/supervisor/adapters/child.test.ts b/src/process/supervisor/adapters/child.test.ts index 691c98d076..b2f7c59fb4 100644 --- a/src/process/supervisor/adapters/child.test.ts +++ b/src/process/supervisor/adapters/child.test.ts @@ -60,8 +60,15 @@ describe("createChildAdapter", () => { options?: { detached?: boolean }; fallbacks?: Array<{ options?: { detached?: boolean } }>; }; - expect(spawnArgs.options?.detached).toBe(true); - expect(spawnArgs.fallbacks?.[0]?.options?.detached).toBe(false); + // On Windows, detached defaults to false (headless Scheduled Task compat); + // on POSIX, detached is true with a no-detach fallback. + if (process.platform === "win32") { + expect(spawnArgs.options?.detached).toBe(false); + expect(spawnArgs.fallbacks).toEqual([]); + } else { + expect(spawnArgs.options?.detached).toBe(true); + expect(spawnArgs.fallbacks?.[0]?.options?.detached).toBe(false); + } adapter.kill(); diff --git a/src/process/supervisor/adapters/child.ts b/src/process/supervisor/adapters/child.ts index 4bd5be0e06..1db291e933 100644 --- a/src/process/supervisor/adapters/child.ts +++ b/src/process/supervisor/adapters/child.ts @@ -42,11 +42,17 @@ export async function createChildAdapter(params: { const stdinMode = params.stdinMode ?? (params.input !== undefined ? "pipe-closed" : "inherit"); + // On Windows, `detached: true` creates a new process group and can prevent + // stdout/stderr pipes from connecting when running under a Scheduled Task + // (headless, no console). Default to `detached: false` on Windows; on + // POSIX systems keep `detached: true` so the child survives parent exit. + const useDetached = process.platform !== "win32"; + const options: SpawnOptions = { cwd: params.cwd, env: params.env ? toStringEnv(params.env) : undefined, stdio: ["pipe", "pipe", "pipe"], - detached: true, + detached: useDetached, windowsHide: true, windowsVerbatimArguments: params.windowsVerbatimArguments, }; @@ -59,12 +65,14 @@ export async function createChildAdapter(params: { const spawned = await spawnWithFallback({ argv: resolvedArgv, options, - fallbacks: [ - { - label: "no-detach", - options: { detached: false }, - }, - ], + fallbacks: useDetached + ? [ + { + label: "no-detach", + options: { detached: false }, + }, + ] + : [], }); const child = spawned.child as ChildProcessWithoutNullStreams;