From 4edf22f63fcafdd08fae52afe7e20d19fbb8951e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 04:37:53 +0100 Subject: [PATCH] fix(acpx): avoid startup agent probes by default --- docs/tools/acp-agents-setup.md | 14 +++++++++----- extensions/acpx/src/service.test.ts | 28 +++++++++++++++++++++++++++- extensions/acpx/src/service.ts | 10 ++++++++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/tools/acp-agents-setup.md b/docs/tools/acp-agents-setup.md index 708801b140b..0c9a321487a 100644 --- a/docs/tools/acp-agents-setup.md +++ b/docs/tools/acp-agents-setup.md @@ -157,7 +157,10 @@ Then verify backend health: ### acpx command and version configuration -By default, the bundled `acpx` plugin uses its plugin-local pinned binary (`node_modules/.bin/acpx` inside the plugin package). Startup registers the backend as not-ready and a background job verifies `acpx --version`; if the binary is missing or mismatched, it runs `npm install --omit=dev --no-save acpx@` and re-verifies. The gateway stays non-blocking throughout. +By default, the bundled `acpx` plugin registers the embedded ACP backend without +spawning an ACP agent during Gateway startup. Run `/acp doctor` for an explicit +live probe. Set `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=1` only when you need the +Gateway to probe the configured agent at startup. Override the command or version in plugin config: @@ -253,10 +256,11 @@ Restart the gateway after changing this value. ### Health probe agent configuration -The bundled `acpx` plugin probes one harness agent while deciding whether the -embedded runtime backend is ready. If `acp.allowedAgents` is set, it defaults to -the first allowed agent; otherwise it defaults to `codex`. If your deployment -needs a different ACP agent for health checks, set the probe agent explicitly: +When `/acp doctor` or the opt-in startup probe checks the backend, the bundled +`acpx` plugin probes one harness agent. If `acp.allowedAgents` is set, it +defaults to the first allowed agent; otherwise it defaults to `codex`. If your +deployment needs a different ACP agent for health checks, set the probe agent +explicitly: ```bash openclaw config set plugins.entries.acpx.config.probeAgent claude diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 9b5f0390842..1d9aab2400c 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -47,6 +47,7 @@ async function makeTempDir(): Promise { afterEach(async () => { runtimeRegistry.clear(); prepareAcpxCodexAuthConfigMock.mockClear(); + delete process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE; delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME; delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE; for (const dir of tempDirs.splice(0)) { @@ -99,7 +100,7 @@ describe("createAcpxRuntimeService", () => { expect(getAcpRuntimeBackend("acpx")).toBeUndefined(); }); - it("creates the embedded runtime state directory before probing", async () => { + it("creates the embedded runtime state directory without probing at startup by default", async () => { const workspaceDir = await makeTempDir(); const stateDir = path.join(workspaceDir, "custom-state"); const ctx = createServiceContext(workspaceDir); @@ -118,7 +119,30 @@ describe("createAcpxRuntimeService", () => { await service.start(ctx); + await fs.access(stateDir); + expect(probeAvailability).not.toHaveBeenCalled(); + expect(getAcpRuntimeBackend("acpx")?.healthy).toBeUndefined(); + + await service.stop?.(ctx); + }); + + it("can run the embedded runtime probe at startup when explicitly enabled", async () => { + process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1"; + const workspaceDir = await makeTempDir(); + const ctx = createServiceContext(workspaceDir); + const probeAvailability = vi.fn(async () => {}); + const runtime = createMockRuntime({ + probeAvailability, + isHealthy: () => true, + }); + const service = createAcpxRuntimeService({ + runtimeFactory: () => runtime as never, + }); + + await service.start(ctx); + expect(probeAvailability).toHaveBeenCalledOnce(); + expect(getAcpRuntimeBackend("acpx")?.healthy?.()).toBe(true); await service.stop?.(ctx); }); @@ -255,6 +279,7 @@ describe("createAcpxRuntimeService", () => { }); it("can skip the embedded runtime probe via env", async () => { + process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1"; process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE = "1"; const workspaceDir = await makeTempDir(); const ctx = createServiceContext(workspaceDir); @@ -277,6 +302,7 @@ describe("createAcpxRuntimeService", () => { }); it("formats non-string doctor details without losing object payloads", async () => { + process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1"; const workspaceDir = await makeTempDir(); const ctx = createServiceContext(workspaceDir); const runtime = createMockRuntime({ diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index df0537497fa..30a47929e51 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -31,6 +31,8 @@ type AcpxRuntimeLike = AcpRuntime & { }>; }; +const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE"; + type AcpxRuntimeFactoryParams = { pluginConfig: ResolvedAcpxPluginConfig; logger?: PluginLogger; @@ -128,6 +130,10 @@ function resolveAllowedAgentsProbeAgent(ctx: OpenClawPluginServiceContext): stri return undefined; } +function shouldRunStartupProbe(env: NodeJS.ProcessEnv = process.env): boolean { + return env[ENABLE_STARTUP_PROBE_ENV] === "1"; +} + export function createAcpxRuntimeService( params: CreateAcpxRuntimeServiceParams = {}, ): OpenClawPluginService { @@ -170,11 +176,11 @@ export function createAcpxRuntimeService( registerAcpRuntimeBackend({ id: ACPX_BACKEND_ID, runtime, - healthy: () => runtime?.isHealthy() ?? false, + ...(shouldRunStartupProbe() ? { healthy: () => runtime?.isHealthy() ?? false } : {}), }); ctx.logger.info(`embedded acpx runtime backend registered (cwd: ${pluginConfig.cwd})`); - if (process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE === "1") { + if (!shouldRunStartupProbe() || process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE === "1") { return; }