fix: detect OpenClaw-managed launchd/systemd services in process respawn

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
This commit is contained in:
taw0002
2026-02-26 10:03:29 -05:00
committed by Peter Steinberger
parent 5c0255477c
commit 792ce7b5b4
2 changed files with 37 additions and 0 deletions

View File

@@ -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();

View File

@@ -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 {