fix(acpx): prevent process cleanup from hanging (#109146)

* fix(acpx): bound process inspection

* docs(changelog): note ACPX cleanup deadline

Co-authored-by: Alix-007 <li.long15@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Alix-007
2026-07-16 10:08:56 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 4432016f7f
commit 7da3bc9872
3 changed files with 41 additions and 1 deletions
+1
View File
@@ -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.
+38 -1
View File
@@ -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<typeof import("openclaw/plugin-sdk/process-runtime")>()),
runExec: runExecMock,
}));
import {
cleanupOpenClawOwnedAcpxProcessTree,
isOpenClawLeaseAwareAcpxProcessCommand,
@@ -61,6 +69,35 @@ function collectMatching<T, U>(
}
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({
+2
View File
@@ -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<AcpxProcessInfo[]> {
const { stdout } = await runExec("ps", ["-axo", "pid=,ppid=,command="], {
logOutput: false,
maxBuffer: 8 * 1024 * 1024,
timeoutMs: ACPX_PROCESS_LIST_TIMEOUT_MS,
});
return parseProcessList(stdout);
}