From 7da3bc987256d68ef668ba6ccae4d48643f346ef Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Fri, 17 Jul 2026 01:08:56 +0800 Subject: [PATCH] fix(acpx): prevent process cleanup from hanging (#109146) * fix(acpx): bound process inspection * docs(changelog): note ACPX cleanup deadline Co-authored-by: Alix-007 --------- Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + extensions/acpx/src/process-reaper.test.ts | 39 +++++++++++++++++++++- extensions/acpx/src/process-reaper.ts | 2 ++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d13125cd64..712367f01264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- **ACPX cleanup process inspection:** bound host process-table reads so stalled `ps` calls cannot hang gateway startup or session cleanup while retaining fail-closed ownership checks. Thanks @Alix-007. - **Cron lifecycle conflict retries:** preserve execution-phase retry decisions across scheduled, manual, and startup-recovered runs so post-execution claim conflicts cannot replay completed messages or tools. Fixes #108428. Thanks @yetval. - **Discord gateway metadata deadline:** carry the existing lookup deadline through DNS and proxy preflight, request headers, and response bodies so stalled gateway startup aborts cleanly. (#104580) Thanks @hugenshen. - **Control UI cloud session thinking:** expose reasoning level in the New Session model picker and persist the selected level before cloud dispatch. diff --git a/extensions/acpx/src/process-reaper.test.ts b/extensions/acpx/src/process-reaper.test.ts index 345b2f6b3880..ec593e1bc62c 100644 --- a/extensions/acpx/src/process-reaper.test.ts +++ b/extensions/acpx/src/process-reaper.test.ts @@ -1,7 +1,15 @@ // ACPX tests cover process reaper plugin behavior. import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js"; + +const runExecMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/process-runtime", async (importOriginal) => ({ + ...(await importOriginal()), + runExec: runExecMock, +})); + import { cleanupOpenClawOwnedAcpxProcessTree, isOpenClawLeaseAwareAcpxProcessCommand, @@ -61,6 +69,35 @@ function collectMatching( } describe("process reaper", () => { + afterEach(() => { + vi.restoreAllMocks(); + runExecMock.mockReset(); + }); + + it("bounds process inspection and fails closed on timeout", async () => { + runExecMock.mockRejectedValueOnce(new Error("process listing timed out")); + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + const killSpy = vi.spyOn(process, "kill"); + + const result = await cleanupOpenClawOwnedAcpxProcessTree({ + rootPid: 200, + rootCommand: CODEX_WRAPPER_COMMAND, + wrapperRoot: WRAPPER_ROOT, + }); + + expect(runExecMock).toHaveBeenCalledWith("ps", ["-axo", "pid=,ppid=,command="], { + logOutput: false, + maxBuffer: 8 * 1024 * 1024, + timeoutMs: 2_000, + }); + expect(result).toEqual({ + inspectedPids: [], + terminatedPids: [], + skippedReason: "unverified-root", + }); + expect(killSpy).not.toHaveBeenCalled(); + }); + it("only treats generated wrappers as launch-lease aware", () => { expect( isOpenClawLeaseAwareAcpxProcessCommand({ diff --git a/extensions/acpx/src/process-reaper.ts b/extensions/acpx/src/process-reaper.ts index d7f915641c12..c72ff6713536 100644 --- a/extensions/acpx/src/process-reaper.ts +++ b/extensions/acpx/src/process-reaper.ts @@ -16,6 +16,7 @@ const GENERATED_WRAPPER_BASENAMES = new Set([ "claude-agent-acp-wrapper.mjs", ]); const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/"; +const ACPX_PROCESS_LIST_TIMEOUT_MS = 2_000; const OWNED_ACP_PACKAGE_NAMES = [ CODEX_ACP_PACKAGE, // Shipped Zed adapter processes can survive a gateway upgrade. Keep cleanup @@ -235,6 +236,7 @@ async function listPlatformProcesses(): Promise { const { stdout } = await runExec("ps", ["-axo", "pid=,ppid=,command="], { logOutput: false, maxBuffer: 8 * 1024 * 1024, + timeoutMs: ACPX_PROCESS_LIST_TIMEOUT_MS, }); return parseProcessList(stdout); }