diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 02d6f64c0be66..28b1897e58d81 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -53,6 +53,7 @@ import * as Stream from "effect/Stream"; import * as Scope from "effect/Scope"; import * as TestClock from "effect/testing/TestClock"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import * as ChildProcess from "effect/process/ChildProcess"; import * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpProtocol from "effect-acp/protocol"; import type * as EffectAcpSchema from "effect-acp/compat"; @@ -571,6 +572,36 @@ function makeMockRuntime(input: { }); } +it.effect("detaches owned ACP processes on POSIX hosts but not on Windows", () => + Effect.gen(function* () { + for (const [platform, expected] of [ + ["win32", false], + ["linux", true], + ] as const) { + let detached: boolean | undefined; + yield* AcpSessionRuntime.make({ + spawn: { command: "acp-agent", args: [] }, + cwd: process.cwd(), + clientInfo: { name: "test", version: "1" }, + ownDetachedProcessGroup: true, + processGroupPlatform: platform, + }).pipe( + Effect.provideService(HostProcess.Platform, platform), + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => { + if (ChildProcess.isStandardCommand(command)) detached = command.options.detached; + return Effect.die("spawn stopped by test"); + }), + ), + Effect.scoped, + Effect.exit, + ); + assert.equal(detached, expected, platform); + } + }).pipe(Effect.provide(layerTest)), +); + const isProviderAdapterProtocolError = Schema.is(ProviderAdapter.ProviderAdapterProtocolError); const makePromptInactivityHarness = Effect.fnUntraced(function* (input: { diff --git a/packages/provider-acp/src/server/AcpSessionRuntime.ts b/packages/provider-acp/src/server/AcpSessionRuntime.ts index b216d6cc88b47..c603dd937fcaf 100644 --- a/packages/provider-acp/src/server/AcpSessionRuntime.ts +++ b/packages/provider-acp/src/server/AcpSessionRuntime.ts @@ -1255,6 +1255,7 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* ( > { const random = yield* SecureRandom.SecureRandom; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const hostPlatform = yield* HostProcess.Platform; const runtimeScope = yield* Scope.fork(yield* Scope.Scope); const eventQueue = yield* Queue.unbounded(); const modeStateRef = yield* Ref.make(undefined); @@ -1464,9 +1465,11 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* ( ...(options.spawn.cwd ? { cwd: options.spawn.cwd } : {}), ...(spawnEnvironment ? { env: spawnEnvironment } : {}), extendEnv: options.spawn.extendEnv ?? true, + // Windows `detached` gives the child its own console, which breaks stdio pipes through + // `.cmd` shims; taskkill /T cleans up the tree there without it. ...(options.ownDetachedProcessGroup === undefined ? {} - : { detached: options.ownDetachedProcessGroup }), + : { detached: options.ownDetachedProcessGroup && hostPlatform !== "win32" }), shell: containedSpawnCommand.shell, }), ) @@ -1628,7 +1631,6 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* ( Effect.uninterruptible(terminateOwnedProcessGroupImpl), ); if (options.ownDetachedProcessGroup === true) { - const hostPlatform = yield* HostProcess.Platform; const forceTerminateOwnedProcessGroup = hostPlatform === "win32" ? terminateWindowsProcessTreeWithTaskkill(Number(child.pid)).pipe(