From c93d6d8daa37fadbbd98611f08c7489b3dc14097 Mon Sep 17 00:00:00 2001 From: mjamiv <142179942+mjamiv@users.noreply.github.com> Date: Sun, 17 May 2026 17:19:05 -0400 Subject: [PATCH] fix(gateway): keep unmanaged restarts in-process (#83138) Summary: - The PR changes ordinary unmanaged gateway restarts to return the existing in-process fallback instead of detached-spawning a replacement child, with focused tests, docs wording, and a changelog entry. - Reproducibility: yes. at source level: current main and v2026.5.12 detach-spawn unmanaged ordinary restarts, ... e PR body also supplies after-fix terminal proof that the patched helper returns disabled without spawning. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for head 8c82df6c776f746cf4fd99ccccfb366cb34e25c6. - Required merge gates passed before the squash merge. Prepared head SHA: 8c82df6c776f746cf4fd99ccccfb366cb34e25c6 Review: https://github.com/openclaw/openclaw/pull/83138#issuecomment-4471071848 Co-authored-by: mjamiv <74088820+mjamiv@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/install/raspberry-pi.md | 2 + docs/vps.md | 2 +- ...latform-notes.startup-optimization.test.ts | 2 +- src/commands/doctor-platform-notes.ts | 2 +- src/infra/process-respawn.test.ts | 48 ++++++++++++------- src/infra/process-respawn.ts | 16 +++---- 7 files changed, 45 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e60d37a020b..774b7d26dec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - CLI/plugins: ship the bundled memory CLI as a package entry so package-installed `openclaw memory` commands register correctly. - CLI/update: defer doctor-time plugin package installs during package swaps and seed post-core repair from the updated install registry, preventing duplicate reinstall failures. - Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing. +- Gateway/restart: keep ordinary unmanaged SIGUSR1/config restarts in-process instead of detach-spawning an orphaned child, preserving custom supervisor PID tracking while leaving update restarts on the fresh-process path. Fixes #65668. - CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c. - Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing `thought_signature` 400s. Fixes #72879. (#80358) Thanks @abnershang. - Memory/QMD: keep lexical search on raw hyphenated queries while normalizing semantic QMD sub-searches, avoiding fallback to the builtin index for dashed identifiers and dates. Fixes #81328. diff --git a/docs/install/raspberry-pi.md b/docs/install/raspberry-pi.md index 4e5c48336393..0cda1fa60952 100644 --- a/docs/install/raspberry-pi.md +++ b/docs/install/raspberry-pi.md @@ -145,6 +145,8 @@ EOF source ~/.bashrc ``` +`OPENCLAW_NO_RESPAWN=1` keeps routine Gateway restarts in-process, which avoids extra process handoffs and keeps PID tracking simple on small hosts. + **Reduce memory usage** -- For headless setups, free GPU memory and disable unused services: ```bash diff --git a/docs/vps.md b/docs/vps.md index d58186de4863..e37ba2f50c04 100644 --- a/docs/vps.md +++ b/docs/vps.md @@ -90,7 +90,7 @@ source ~/.bashrc ``` - `NODE_COMPILE_CACHE` improves repeated command startup times. -- `OPENCLAW_NO_RESPAWN=1` avoids extra startup overhead from a self-respawn path. +- `OPENCLAW_NO_RESPAWN=1` keeps routine Gateway restarts in-process, which avoids extra process handoffs and keeps PID tracking simple on small hosts. - First command run warms the cache; subsequent runs are faster. - For Raspberry Pi specifics, see [Raspberry Pi](/install/raspberry-pi). diff --git a/src/commands/doctor-platform-notes.startup-optimization.test.ts b/src/commands/doctor-platform-notes.startup-optimization.test.ts index 763e7c29fffb..cceb35716f56 100644 --- a/src/commands/doctor-platform-notes.startup-optimization.test.ts +++ b/src/commands/doctor-platform-notes.startup-optimization.test.ts @@ -36,7 +36,7 @@ describe("noteStartupOptimizationHints", () => { expect(message).toBe( [ "- NODE_COMPILE_CACHE points to /tmp; use /var/tmp so cache survives reboots and warms startup reliably.", - "- OPENCLAW_NO_RESPAWN is not set to 1; set it to avoid extra startup overhead from self-respawn.", + "- OPENCLAW_NO_RESPAWN is not set to 1; set it when you want routine gateway restarts to stay in-process instead of handing off to a managed supervisor.", "- Suggested env for low-power hosts:", " export NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache", " mkdir -p /var/tmp/openclaw-compile-cache", diff --git a/src/commands/doctor-platform-notes.ts b/src/commands/doctor-platform-notes.ts index 52a2f2180ece..c017d3b31b06 100644 --- a/src/commands/doctor-platform-notes.ts +++ b/src/commands/doctor-platform-notes.ts @@ -197,7 +197,7 @@ export function noteStartupOptimizationHints( if (noRespawn !== "1") { lines.push( - "- OPENCLAW_NO_RESPAWN is not set to 1; set it to avoid extra startup overhead from self-respawn.", + "- OPENCLAW_NO_RESPAWN is not set to 1; set it when you want routine gateway restarts to stay in-process instead of handing off to a managed supervisor.", ); } diff --git a/src/infra/process-respawn.test.ts b/src/infra/process-respawn.test.ts index a3f3d5d669b9..eac1b227c0a2 100644 --- a/src/infra/process-respawn.test.ts +++ b/src/infra/process-respawn.test.ts @@ -133,7 +133,7 @@ describe("restartGatewayProcessWithFreshPid", () => { expect(spawnMock).not.toHaveBeenCalled(); }); - it("spawns detached child with current exec argv", () => { + it("uses in-process restart on unmanaged Unix so custom supervisors keep the tracked PID", () => { delete process.env.OPENCLAW_NO_RESPAWN; clearSupervisorHints(); setPlatform("linux"); @@ -143,16 +143,11 @@ describe("restartGatewayProcessWithFreshPid", () => { const result = restartGatewayProcessWithFreshPid(); - expect(result).toEqual({ mode: "spawned", pid: 4242 }); - expect(spawnMock).toHaveBeenCalledWith( - process.execPath, - ["--import", "tsx", "/repo/dist/index.js", "gateway", "run"], - { - detached: true, - env: process.env, - stdio: "inherit", - }, - ); + expect(result).toEqual({ + mode: "disabled", + detail: "unmanaged: use in-process restart to keep custom supervisor PID tracking stable", + }); + expect(spawnMock).not.toHaveBeenCalled(); }); it("returns supervised when OPENCLAW_LAUNCHD_LABEL is set (stock launchd plist)", () => { @@ -186,12 +181,15 @@ describe("restartGatewayProcessWithFreshPid", () => { setPlatform("linux"); process.env.OPENCLAW_SERVICE_MARKER = "openclaw"; process.env.OPENCLAW_SERVICE_KIND = "gateway"; - spawnMock.mockReturnValue({ pid: 4242, unref: vi.fn() }); const result = restartGatewayProcessWithFreshPid(); - expect(result).toEqual({ mode: "spawned", pid: 4242 }); + expect(result).toEqual({ + mode: "disabled", + detail: "unmanaged: use in-process restart to keep custom supervisor PID tracking stable", + }); expect(triggerOpenClawRestartMock).not.toHaveBeenCalled(); + expect(spawnMock).not.toHaveBeenCalled(); }); it("returns disabled on Windows without Scheduled Task markers", () => { @@ -235,7 +233,7 @@ describe("restartGatewayProcessWithFreshPid", () => { expect(spawnMock).not.toHaveBeenCalled(); }); - it("returns failed when spawn throws", () => { + it("does not attempt detached spawn on unmanaged Unix even if spawn would throw", () => { delete process.env.OPENCLAW_NO_RESPAWN; clearSupervisorHints(); setPlatform("linux"); @@ -244,8 +242,11 @@ describe("restartGatewayProcessWithFreshPid", () => { throw new Error("spawn failed"); }); const result = restartGatewayProcessWithFreshPid(); - expect(result.mode).toBe("failed"); - expect(result.detail).toContain("spawn failed"); + expect(result).toEqual({ + mode: "disabled", + detail: "unmanaged: use in-process restart to keep custom supervisor PID tracking stable", + }); + expect(spawnMock).not.toHaveBeenCalled(); }); }); @@ -286,4 +287,19 @@ describe("respawnGatewayProcessForUpdate", () => { }, ); }); + + it("returns failed when update detached respawn throws", () => { + delete process.env.OPENCLAW_NO_RESPAWN; + clearSupervisorHints(); + setPlatform("linux"); + + spawnMock.mockImplementation(() => { + throw new Error("spawn failed"); + }); + + const result = respawnGatewayProcessForUpdate(); + + expect(result.mode).toBe("failed"); + expect(result.detail).toContain("spawn failed"); + }); }); diff --git a/src/infra/process-respawn.ts b/src/infra/process-respawn.ts index 0d2582a52b7c..e823e35e9285 100644 --- a/src/infra/process-respawn.ts +++ b/src/infra/process-respawn.ts @@ -43,10 +43,11 @@ function spawnDetachedGatewayProcess(opts: GatewayRespawnOptions = {}): { * Attempt to restart this process with a fresh PID. * - supervised environments (launchd/systemd/schtasks): caller should exit and let supervisor restart * - OPENCLAW_NO_RESPAWN=1: caller should keep in-process restart behavior (tests/dev) - * - otherwise: spawn detached child with current argv/execArgv, then caller exits + * - unmanaged environments: caller should keep in-process restart behavior so + * custom supervisors keep tracking the same gateway PID */ export function restartGatewayProcessWithFreshPid( - opts: GatewayRespawnOptions = {}, + _opts: GatewayRespawnOptions = {}, ): GatewayRespawnResult { if (isTruthy(process.env.OPENCLAW_NO_RESPAWN)) { return { mode: "disabled" }; @@ -82,13 +83,10 @@ export function restartGatewayProcessWithFreshPid( }; } - try { - const { pid } = spawnDetachedGatewayProcess(opts); - return { mode: "spawned", pid }; - } catch (err) { - const detail = formatErrorMessage(err); - return { mode: "failed", detail }; - } + return { + mode: "disabled", + detail: "unmanaged: use in-process restart to keep custom supervisor PID tracking stable", + }; } /**