From e20f44509924b961e51b16338b5b63a831ad173b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 21:36:07 +0000 Subject: [PATCH] fix(supervisor): keep service-managed children attached (#38463, thanks @spirittechie) Co-authored-by: Jesse Paul --- CHANGELOG.md | 1 + src/process/supervisor/adapters/child.test.ts | 20 +++++++++++++++++++ src/process/supervisor/adapters/child.ts | 13 +++++++----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d56475d7dca..8d3773321c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -282,6 +282,7 @@ Docs: https://docs.openclaw.ai - Config/env substitution degraded mode: convert missing `${VAR}` resolution in config reads from hard-fail to warning-backed degraded behavior, while preventing unresolved placeholders from being accepted as gateway credentials. (#39050) Thanks @akz142857. - Discord inbound listener non-blocking dispatch: make `MESSAGE_CREATE` listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki. - Daemon/Windows PATH freeze fix: stop persisting install-time `PATH` snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo. +- Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie. ## 2026.3.2 diff --git a/src/process/supervisor/adapters/child.test.ts b/src/process/supervisor/adapters/child.test.ts index 9c46bdd0cd7..88885800b57 100644 --- a/src/process/supervisor/adapters/child.test.ts +++ b/src/process/supervisor/adapters/child.test.ts @@ -49,6 +49,8 @@ async function createAdapterHarness(params?: { } describe("createChildAdapter", () => { + const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER; + beforeAll(async () => { ({ createChildAdapter } = await import("./child.js")); }); @@ -56,6 +58,11 @@ describe("createChildAdapter", () => { beforeEach(() => { spawnWithFallbackMock.mockClear(); killProcessTreeMock.mockClear(); + if (originalServiceMarker === undefined) { + delete process.env.OPENCLAW_SERVICE_MARKER; + } else { + process.env.OPENCLAW_SERVICE_MARKER = originalServiceMarker; + } }); it("uses process-tree kill for default SIGKILL", async () => { @@ -90,6 +97,19 @@ describe("createChildAdapter", () => { expect(killMock).toHaveBeenCalledWith("SIGTERM"); }); + it("disables detached mode in service-managed runtime", async () => { + process.env.OPENCLAW_SERVICE_MARKER = "openclaw"; + + await createAdapterHarness({ pid: 7777 }); + + const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as { + options?: { detached?: boolean }; + fallbacks?: Array<{ options?: { detached?: boolean } }>; + }; + expect(spawnArgs.options?.detached).toBe(false); + expect(spawnArgs.fallbacks ?? []).toEqual([]); + }); + it("keeps inherited env when no override env is provided", async () => { await createAdapterHarness({ pid: 3333, diff --git a/src/process/supervisor/adapters/child.ts b/src/process/supervisor/adapters/child.ts index a6db4329336..44275df6e64 100644 --- a/src/process/supervisor/adapters/child.ts +++ b/src/process/supervisor/adapters/child.ts @@ -21,6 +21,10 @@ function resolveCommand(command: string): string { export type ChildAdapter = SpawnProcessAdapter; +function isServiceManagedRuntime(): boolean { + return Boolean(process.env.OPENCLAW_SERVICE_MARKER?.trim()); +} + export async function createChildAdapter(params: { argv: string[]; cwd?: string; @@ -34,11 +38,10 @@ 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"; + // In service-managed mode keep children attached so systemd/launchd can + // stop the full process tree reliably. Outside service mode preserve the + // existing POSIX detached behavior. + const useDetached = process.platform !== "win32" && !isServiceManagedRuntime(); const options: SpawnOptions = { cwd: params.cwd,