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);
}