From 792ce7b5b456745f83c8e684b5a75f10795bce24 Mon Sep 17 00:00:00 2001 From: taw0002 Date: Thu, 26 Feb 2026 10:03:29 -0500 Subject: [PATCH] fix: detect OpenClaw-managed launchd/systemd services in process respawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restartGatewayProcessWithFreshPid() checks SUPERVISOR_HINT_ENV_VARS to decide whether to let the supervisor handle the restart (mode=supervised) or to fork a detached child (mode=spawned). The existing list only had native launchd vars (LAUNCH_JOB_LABEL, LAUNCH_JOB_NAME) and systemd vars (INVOCATION_ID, SYSTEMD_EXEC_PID, JOURNAL_STREAM). macOS launchd does NOT automatically inject LAUNCH_JOB_LABEL into the child environment. OpenClaw's own plist generator (buildServiceEnvironment in service-env.ts) sets OPENCLAW_LAUNCHD_LABEL instead. So on stock macOS LaunchAgent installs, isLikelySupervisedProcess() returned false, causing the gateway to fork a detached child on SIGUSR1 restart. The original process then exits, launchd sees its child died, respawns a new instance which finds the orphan holding the port — infinite crash loop. Fix: add OPENCLAW_LAUNCHD_LABEL, OPENCLAW_SYSTEMD_UNIT, and OPENCLAW_SERVICE_MARKER to the supervisor hint list. These are set by OpenClaw's own service environment builders for both launchd and systemd and are the reliable supervised-mode signals. Fixes #27605 --- src/infra/process-respawn.test.ts | 27 +++++++++++++++++++++++++++ src/infra/process-respawn.ts | 10 ++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/infra/process-respawn.test.ts b/src/infra/process-respawn.test.ts index 90e9b5a9c..e92463ad0 100644 --- a/src/infra/process-respawn.test.ts +++ b/src/infra/process-respawn.test.ts @@ -23,9 +23,12 @@ afterEach(() => { function clearSupervisorHints() { delete process.env.LAUNCH_JOB_LABEL; delete process.env.LAUNCH_JOB_NAME; + delete process.env.OPENCLAW_LAUNCHD_LABEL; delete process.env.INVOCATION_ID; delete process.env.SYSTEMD_EXEC_PID; delete process.env.JOURNAL_STREAM; + delete process.env.OPENCLAW_SYSTEMD_UNIT; + delete process.env.OPENCLAW_SERVICE_MARKER; } describe("restartGatewayProcessWithFreshPid", () => { @@ -63,6 +66,30 @@ describe("restartGatewayProcessWithFreshPid", () => { ); }); + it("returns supervised when OPENCLAW_LAUNCHD_LABEL is set (stock launchd plist)", () => { + clearSupervisorHints(); + process.env.OPENCLAW_LAUNCHD_LABEL = "ai.openclaw.gateway"; + const result = restartGatewayProcessWithFreshPid(); + expect(result.mode).toBe("supervised"); + expect(spawnMock).not.toHaveBeenCalled(); + }); + + it("returns supervised when OPENCLAW_SYSTEMD_UNIT is set", () => { + clearSupervisorHints(); + process.env.OPENCLAW_SYSTEMD_UNIT = "openclaw-gateway.service"; + const result = restartGatewayProcessWithFreshPid(); + expect(result.mode).toBe("supervised"); + expect(spawnMock).not.toHaveBeenCalled(); + }); + + it("returns supervised when OPENCLAW_SERVICE_MARKER is set", () => { + clearSupervisorHints(); + process.env.OPENCLAW_SERVICE_MARKER = "gateway"; + const result = restartGatewayProcessWithFreshPid(); + expect(result.mode).toBe("supervised"); + expect(spawnMock).not.toHaveBeenCalled(); + }); + it("returns failed when spawn throws", () => { delete process.env.OPENCLAW_NO_RESPAWN; clearSupervisorHints(); diff --git a/src/infra/process-respawn.ts b/src/infra/process-respawn.ts index 3c6ef3710..8ad4e1992 100644 --- a/src/infra/process-respawn.ts +++ b/src/infra/process-respawn.ts @@ -9,11 +9,21 @@ export type GatewayRespawnResult = { }; const SUPERVISOR_HINT_ENV_VARS = [ + // macOS launchd — native env vars (may be set by launchd itself) "LAUNCH_JOB_LABEL", "LAUNCH_JOB_NAME", + // macOS launchd — OpenClaw's own plist generator sets these via + // buildServiceEnvironment() in service-env.ts. launchd does NOT + // automatically inject LAUNCH_JOB_LABEL into the child environment, + // so OPENCLAW_LAUNCHD_LABEL is the reliable supervised-mode signal. + "OPENCLAW_LAUNCHD_LABEL", + // Linux systemd "INVOCATION_ID", "SYSTEMD_EXEC_PID", "JOURNAL_STREAM", + "OPENCLAW_SYSTEMD_UNIT", + // Generic service marker (set by both launchd and systemd plist/unit generators) + "OPENCLAW_SERVICE_MARKER", ]; function isTruthy(value: string | undefined): boolean {