

























@@ -383,68 +383,6 @@ describe.skipIf(isWindows)("restart-stale-pids", () => {
383383},
384384);
385385386-it("excludes the inherited gateway service PID when the caller has been reparented to launchd — suicide-kill regression", () => {
387-// Regression for the rh-bot SIGKILL cycle diagnosed 2026-06-05.
388-//
389-// Scenario: a user `~/.zshrc` contains a legacy auto-start block
390-// `(openclaw gateway >/dev/null 2>&1 &)`
391-// and codex 0.135.0 spawns `/bin/zsh -c "set -e; . shell_snapshot"`
392-// per turn for tool execution context. The backgrounded subshell
393-// exits, the new `openclaw gateway` is reparented to launchd
394-// (process.ppid === 1), and the ancestor walk can no longer see the
395-// launchd-managed parent gateway. lsof reports the parent gateway
396-// on the port, the cleanup SIGKILLs it, launchd respawns it via
397-// `KeepAlive: { Crashed: true }`, and the cycle repeats on every
398-// chat request.
399-//
400-// Fix: the gateway-cli captures `OPENCLAW_GATEWAY_SERVICE_PID` from
401-// its inherited env BEFORE overwriting it with `process.pid`, then
402-// hands the captured value to `cleanStaleGatewayProcessesSync`.
403-// The cleanup adds it to the exclusion set so the parent gateway
404-// is left alone even though it is now an unrelated sibling on the
405-// port from the caller's perspective.
406-const launchdManagedGatewayPid = process.pid + 4001;
407-const unrelatedStalePid = process.pid + 4002;
408-mockSpawnSync.mockReturnValue({
409-error: null,
410-status: 0,
411-stdout: lsofOutput([
412-{ pid: launchdManagedGatewayPid, cmd: "openclaw-gateway" },
413-{ pid: unrelatedStalePid, cmd: "openclaw-gateway" },
414-]),
415-stderr: "",
416-});
417-// Stub ppid to 1 (launchd) so the ancestor walk cannot reach the
418-// running gateway. Without the inherited-PID protection, the
419-// launchd-managed gateway PID would be returned and killed.
420-const pids = withStubbedPpid(1, () =>
421-findGatewayPidsOnPortSync(18789, undefined, launchdManagedGatewayPid),
422-);
423-expect(pids).not.toContain(launchdManagedGatewayPid);
424-expect(pids).toContain(unrelatedStalePid);
425-});
426-427-it("ignores a non-positive inherited gateway PID rather than poisoning the kill list", () => {
428-// Defensive: callers may pass undefined or zero when the env var is
429-// missing or malformed. The helper must treat those as no-op rather
430-// than excluding pid 0 (which would silently mask a genuine kernel
431-// pid 0 listener — there is no such thing in practice, but the
432-// guard documents the contract).
433-const stalePid = process.pid + 4101;
434-mockSpawnSync.mockReturnValue({
435-error: null,
436-status: 0,
437-stdout: lsofOutput([{ pid: stalePid, cmd: "openclaw-gateway" }]),
438-stderr: "",
439-});
440-const pidsUndef = findGatewayPidsOnPortSync(18789, undefined, undefined);
441-const pidsZero = findGatewayPidsOnPortSync(18789, undefined, 0);
442-const pidsNegative = findGatewayPidsOnPortSync(18789, undefined, -1);
443-expect(pidsUndef).toContain(stalePid);
444-expect(pidsZero).toContain(stalePid);
445-expect(pidsNegative).toContain(stalePid);
446-});
447-448386it("excludes PID 1 when the direct parent gateway is the container entrypoint — container topology", () => {
449387// Codex P1: in container deployments the gateway is the container
450388// entrypoint and therefore runs as PID 1 of its namespace. A sidecar
@@ -860,6 +798,32 @@ describe.skipIf(isWindows)("restart-stale-pids", () => {
860798expect(killSpy).toHaveBeenCalledWith(stalePid, "SIGTERM");
861799});
862800801+it("does not kill a protected gateway pid after reparenting", () => {
802+const protectedPid = process.pid + 4001;
803+const stalePid = process.pid + 4002;
804+let lsofCall = 0;
805+mockSpawnSync.mockImplementation(() => {
806+lsofCall += 1;
807+return lsofCall === 1
808+ ? createLsofResult({
809+stdout: lsofOutput([
810+{ pid: protectedPid, cmd: "openclaw-gateway" },
811+{ pid: stalePid, cmd: "openclaw-gateway" },
812+]),
813+})
814+ : createLsofResult({ status: 1 });
815+});
816+const killSpy = vi.spyOn(process, "kill").mockReturnValue(true);
817+818+const result = withStubbedPpid(1, () =>
819+cleanStaleGatewayProcessesSync(18789, { protectedPid }),
820+);
821+822+expect(result).toEqual([stalePid]);
823+expect(killSpy).toHaveBeenCalledWith(stalePid, "SIGTERM");
824+expect(killSpy).not.toHaveBeenCalledWith(protectedPid, expect.anything());
825+});
826+863827it("escalates to SIGKILL when process survives the SIGTERM window", () => {
864828const stalePid = process.pid + 101;
865829let call = 0;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。