fix: raise acpx runtime timeout

This commit is contained in:
Peter Steinberger
2026-04-08 02:35:57 +01:00
parent c5392f3640
commit 6211e3dcd6
6 changed files with 66 additions and 5 deletions

View File

@@ -774,6 +774,19 @@ Security and trust notes:
Custom `mcpServers` still work as before. The built-in plugin-tools bridge is an
additional opt-in convenience, not a replacement for generic MCP server config.
### Runtime timeout configuration
The bundled `acpx` plugin defaults embedded runtime turns to a 120-second
timeout. This gives slower harnesses such as Gemini CLI enough time to complete
ACP startup and initialization. Override it if your host needs a different
runtime limit:
```bash
openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
```
Restart the gateway after changing this value.
## Permission configuration
ACP sessions run non-interactively — there is no TTY to approve or deny file-write and shell-exec permission prompts. The acpx plugin provides two config keys that control how permissions are handled:

View File

@@ -32,7 +32,8 @@
},
"timeoutSeconds": {
"type": "number",
"minimum": 0.001
"minimum": 0.001,
"default": 120
},
"queueOwnerTtlSeconds": {
"type": "number",
@@ -106,7 +107,7 @@
},
"timeoutSeconds": {
"label": "Prompt Timeout Seconds",
"help": "Optional timeout for each embedded runtime turn.",
"help": "Timeout for each embedded runtime turn. Defaults to 120 seconds so slower Gemini CLI ACP startups have room to initialize.",
"advanced": true
},
"queueOwnerTtlSeconds": {

View File

@@ -8,6 +8,8 @@ export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number];
export const ACPX_NON_INTERACTIVE_POLICIES = ["deny", "fail"] as const;
export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_POLICIES)[number];
export const DEFAULT_ACPX_TIMEOUT_SECONDS = 120;
export type McpServerConfig = {
command: string;
args?: string[];
@@ -92,7 +94,7 @@ export const AcpxPluginConfigSchema = z.strictObject({
timeoutSeconds: z
.number({ error: "timeoutSeconds must be a number >= 0.001" })
.min(0.001, { error: "timeoutSeconds must be a number >= 0.001" })
.optional(),
.default(DEFAULT_ACPX_TIMEOUT_SECONDS),
queueOwnerTtlSeconds: z
.number({ error: "queueOwnerTtlSeconds must be a number >= 0" })
.min(0, { error: "queueOwnerTtlSeconds must be a number >= 0" })

View File

@@ -15,9 +15,21 @@ describe("embedded acpx plugin config", () => {
expect(resolved.stateDir).toBe(path.join(workspaceDir, "state"));
expect(resolved.permissionMode).toBe("approve-reads");
expect(resolved.nonInteractivePermissions).toBe("fail");
expect(resolved.timeoutSeconds).toBe(120);
expect(resolved.agents).toEqual({});
});
it("keeps explicit timeoutSeconds config", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
timeoutSeconds: 300,
},
workspaceDir: "/tmp/openclaw-acpx",
});
expect(resolved.timeoutSeconds).toBe(300);
});
it("accepts agent command overrides", () => {
const resolved = resolveAcpxPluginConfig({
rawConfig: {
@@ -62,6 +74,9 @@ describe("embedded acpx plugin config", () => {
properties: expect.objectContaining({
cwd: expect.any(Object),
stateDir: expect.any(Object),
timeoutSeconds: expect.objectContaining({
default: 120,
}),
agents: expect.any(Object),
mcpServers: expect.any(Object),
}),

View File

@@ -3,7 +3,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { z } from "openclaw/plugin-sdk/zod";
import { AcpxPluginConfigSchema } from "./config-schema.js";
import { AcpxPluginConfigSchema, DEFAULT_ACPX_TIMEOUT_SECONDS } from "./config-schema.js";
import type {
AcpxPluginConfig,
AcpxPermissionMode,
@@ -238,7 +238,7 @@ export function resolveAcpxPluginConfig(params: {
pluginToolsMcpBridge,
strictWindowsCmdWrapper:
normalized.strictWindowsCmdWrapper ?? DEFAULT_STRICT_WINDOWS_CMD_WRAPPER,
timeoutSeconds: normalized.timeoutSeconds,
timeoutSeconds: normalized.timeoutSeconds ?? DEFAULT_ACPX_TIMEOUT_SECONDS,
queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS,
legacyCompatibilityConfig: {
strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper,

View File

@@ -112,6 +112,36 @@ describe("createAcpxRuntimeService", () => {
await service.stop?.(ctx);
});
it("passes the default runtime timeout to the embedded runtime factory", async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = {
ensureSession: vi.fn(),
runTurn: vi.fn(),
cancel: vi.fn(),
close: vi.fn(),
probeAvailability: vi.fn(async () => {}),
isHealthy: vi.fn(() => true),
doctor: vi.fn(async () => ({ ok: true, message: "ok" })),
};
const runtimeFactory = vi.fn(() => runtime as never);
const service = createAcpxRuntimeService({
runtimeFactory,
});
await service.start(ctx);
expect(runtimeFactory).toHaveBeenCalledWith(
expect.objectContaining({
pluginConfig: expect.objectContaining({
timeoutSeconds: 120,
}),
}),
);
await service.stop?.(ctx);
});
it("warns when legacy compatibility config is explicitly ignored", async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);