mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix: keep ACP turns on OpenClaw timeouts (#82997)
This commit is contained in:
committed by
GitHub
parent
e4ec1b3de8
commit
83e19ca469
@@ -274,18 +274,19 @@ What this does:
|
||||
- Exposes selected built-in OpenClaw tools. The initial server exposes `cron`.
|
||||
- Keeps core-tool exposure explicit and default-off.
|
||||
|
||||
### Runtime timeout configuration
|
||||
### Runtime operation timeout configuration
|
||||
|
||||
The `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:
|
||||
The `acpx` plugin gives embedded runtime startup and control operations 120
|
||||
seconds by default. This gives slower harnesses such as Gemini CLI enough time
|
||||
to complete ACP startup and initialization. Override it if your host needs a
|
||||
different operation limit:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.acpx.config.timeoutSeconds 180
|
||||
```
|
||||
|
||||
Restart the gateway after changing this value.
|
||||
Runtime turns use OpenClaw agent/run timeouts, including `/acp timeout` and
|
||||
`sessions_spawn.timeoutSeconds`. Restart the gateway after changing this value.
|
||||
|
||||
### Health probe agent configuration
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@
|
||||
"advanced": true
|
||||
},
|
||||
"timeoutSeconds": {
|
||||
"label": "Prompt Timeout Seconds",
|
||||
"help": "Timeout for each embedded runtime turn. Defaults to 120 seconds so slower Gemini CLI ACP startups have room to initialize.",
|
||||
"label": "Runtime Operation Timeout Seconds",
|
||||
"help": "Timeout for embedded ACP runtime startup and control operations. ACP turns use OpenClaw agent/run timeouts.",
|
||||
"advanced": true
|
||||
},
|
||||
"queueOwnerTtlSeconds": {
|
||||
|
||||
@@ -535,6 +535,82 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("disables delegate prompt timeout for OpenClaw-managed turns", async () => {
|
||||
const baseStore: TestSessionStore = {
|
||||
load: vi.fn(async () => ({
|
||||
acpxRecordId: "agent:codex:acp:test",
|
||||
agentCommand: CODEX_ACP_COMMAND,
|
||||
})),
|
||||
save: vi.fn(async () => {}),
|
||||
};
|
||||
const { runtime, delegate } = makeRuntime(baseStore, {
|
||||
timeoutMs: 1,
|
||||
agentRegistry: {
|
||||
resolve: (agentName: string) => (agentName === "codex" ? CODEX_ACP_COMMAND : agentName),
|
||||
list: () => ["codex"],
|
||||
},
|
||||
});
|
||||
const runTurn = vi.spyOn(delegate, "runTurn").mockImplementation(async function* () {
|
||||
yield { type: "done" };
|
||||
});
|
||||
const startTurn = vi.spyOn(delegate, "startTurn").mockImplementation(
|
||||
(input): AcpRuntimeTurn => ({
|
||||
requestId: input.requestId,
|
||||
events: (async function* () {
|
||||
yield { type: "done" as const, stopReason: "end_turn" };
|
||||
})(),
|
||||
result: Promise.resolve({
|
||||
status: "completed" as const,
|
||||
stopReason: "end_turn",
|
||||
}),
|
||||
cancel: vi.fn(async () => {}),
|
||||
closeStream: vi.fn(async () => {}),
|
||||
}),
|
||||
);
|
||||
|
||||
for await (const _event of runtime.runTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
runtimeSessionName: "agent:codex:acp:test",
|
||||
acpxRecordId: "agent:codex:acp:test",
|
||||
},
|
||||
text: "Reply exactly OK",
|
||||
mode: "prompt",
|
||||
requestId: "turn-1",
|
||||
})) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
expect(runTurn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
timeoutMs: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const turn = runtime.startTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
runtimeSessionName: "agent:codex:acp:test",
|
||||
acpxRecordId: "agent:codex:acp:test",
|
||||
},
|
||||
text: "Reply exactly OK",
|
||||
mode: "prompt",
|
||||
requestId: "turn-2",
|
||||
});
|
||||
for await (const _event of turn.events) {
|
||||
// no-op
|
||||
}
|
||||
await turn.result;
|
||||
|
||||
expect(startTurn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
timeoutMs: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not normalize model startup for non-Codex ACP agents", async () => {
|
||||
const baseStore: TestSessionStore = {
|
||||
load: vi.fn(async () => undefined),
|
||||
|
||||
@@ -51,6 +51,15 @@ type ResetAwareSessionStore = AcpSessionStore & {
|
||||
markFresh: (sessionKey: string) => void;
|
||||
};
|
||||
|
||||
function withOpenClawManagedTurnTimeout<T extends object>(input: T): T & { timeoutMs: 0 } {
|
||||
// OpenClaw owns ACP turn deadlines. acpx treats timeout after partial agent
|
||||
// output as a completed turn, which can mark background work done early.
|
||||
return {
|
||||
...input,
|
||||
timeoutMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
type AcpxLaunchLeaseContext = {
|
||||
leaseId: string;
|
||||
gatewayInstanceId: string;
|
||||
@@ -989,7 +998,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
const command = await this.resolveCommandForHandle(input.handle);
|
||||
const delegate = await this.resolveDelegateForHandle(input.handle);
|
||||
try {
|
||||
for await (const event of delegate.runTurn(input)) {
|
||||
for await (const event of delegate.runTurn(withOpenClawManagedTurnTimeout(input))) {
|
||||
if (
|
||||
event.type !== "error" ||
|
||||
!isCodexAcpCommand(command) ||
|
||||
@@ -1035,7 +1044,7 @@ export class AcpxRuntime implements AcpRuntime {
|
||||
try {
|
||||
return {
|
||||
command,
|
||||
turn: delegate.startTurn(input),
|
||||
turn: delegate.startTurn(withOpenClawManagedTurnTimeout(input)),
|
||||
};
|
||||
} catch (error) {
|
||||
if (!isCodexAcpCommand(command) || !isGenericInternalAcpError(error)) {
|
||||
|
||||
@@ -483,6 +483,36 @@ describe("createAcpxRuntimeService", () => {
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("passes the plugin timeout to the default acpx runtime constructor", async () => {
|
||||
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "0";
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
const service = createAcpxRuntimeService({
|
||||
pluginConfig: { timeoutSeconds: 0.001 },
|
||||
});
|
||||
|
||||
await service.start(ctx);
|
||||
|
||||
const backend = getAcpRuntimeBackend("acpx");
|
||||
if (!backend) {
|
||||
throw new Error("expected ACPX runtime backend");
|
||||
}
|
||||
const backendRuntime = backend.runtime as {
|
||||
ensureSession(input: { agent: string; mode: string; sessionKey: string }): Promise<unknown>;
|
||||
};
|
||||
|
||||
await backendRuntime.ensureSession({
|
||||
agent: "codex",
|
||||
mode: "oneshot",
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
});
|
||||
|
||||
const [options] = acpxRuntimeConstructorMock.mock.calls[0] ?? [];
|
||||
expect(options).toHaveProperty("timeoutMs", 1);
|
||||
|
||||
await service.stop?.(ctx);
|
||||
});
|
||||
|
||||
it("runs the embedded runtime probe at startup by default and reports health", async () => {
|
||||
const workspaceDir = await makeTempDir();
|
||||
const ctx = createServiceContext(workspaceDir);
|
||||
|
||||
@@ -679,7 +679,7 @@ describe("AcpSessionManager", () => {
|
||||
expect(runtimeState.runTurn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("times out a hung persistent turn without closing the session and lets queued work continue", async () => {
|
||||
it("times out a hung persistent turn after partial progress without closing the session and lets queued work continue", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const runtimeState = createRuntime();
|
||||
@@ -697,6 +697,7 @@ describe("AcpSessionManager", () => {
|
||||
runtimeState.runTurn.mockImplementation(async function* (input: { requestId: string }) {
|
||||
if (input.requestId === "r1") {
|
||||
firstTurnStarted = true;
|
||||
yield { type: "text_delta" as const, text: "Working on it..." };
|
||||
await new Promise(() => {});
|
||||
}
|
||||
yield { type: "done" as const };
|
||||
|
||||
Reference in New Issue
Block a user