diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 4df9452b856..3075119d0fb 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -108,6 +108,7 @@ import { import { resolveSystemPromptOverride } from "../../system-prompt-override.js"; import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; +import { resolveAgentTimeoutMs } from "../../timeout.js"; import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js"; import { resolveTranscriptPolicy } from "../../transcript-policy.js"; import { normalizeUsage, type NormalizedUsage, type UsageLike } from "../../usage.js"; @@ -1249,9 +1250,13 @@ export async function runEmbeddedAttempt( let idleTimeoutTrigger: ((error: Error) => void) | undefined; // Wrap stream with idle timeout detection + const configuredRunTimeoutMs = resolveAgentTimeoutMs({ + cfg: params.config, + }); const idleTimeoutMs = resolveLlmIdleTimeoutMs({ cfg: params.config, trigger: params.trigger, + runTimeoutMs: params.timeoutMs !== configuredRunTimeoutMs ? params.timeoutMs : undefined, }); if (idleTimeoutMs > 0) { activeSession.agent.streamFn = streamWithIdleTimeout( diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts b/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts index 3d82881ac02..ea3d51d5770 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts +++ b/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts @@ -55,6 +55,14 @@ describe("resolveLlmIdleTimeoutMs", () => { expect(resolveLlmIdleTimeoutMs({ cfg })).toBe(300_000); }); + it("uses an explicit run timeout override when llm.idleTimeoutSeconds is not set", () => { + expect(resolveLlmIdleTimeoutMs({ runTimeoutMs: 900_000 })).toBe(900_000); + }); + + it("disables the idle watchdog when an explicit run timeout disables timeouts", () => { + expect(resolveLlmIdleTimeoutMs({ runTimeoutMs: 2_147_000_000 })).toBe(0); + }); + it("prefers llm.idleTimeoutSeconds over agents.defaults.timeoutSeconds", () => { const cfg = { agents: { defaults: { timeoutSeconds: 300, llm: { idleTimeoutSeconds: 120 } } }, @@ -62,6 +70,13 @@ describe("resolveLlmIdleTimeoutMs", () => { expect(resolveLlmIdleTimeoutMs({ cfg })).toBe(120_000); }); + it("prefers llm.idleTimeoutSeconds over an explicit run timeout override", () => { + const cfg = { + agents: { defaults: { llm: { idleTimeoutSeconds: 120 } } }, + } as OpenClawConfig; + expect(resolveLlmIdleTimeoutMs({ cfg, runTimeoutMs: 900_000 })).toBe(120_000); + }); + it("keeps idleTimeoutSeconds=0 disabled even when timeoutSeconds is set", () => { const cfg = { agents: { defaults: { timeoutSeconds: 300, llm: { idleTimeoutSeconds: 0 } } }, diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts b/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts index 5f346fea3fa..fec7c8ef1e6 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts +++ b/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts @@ -21,14 +21,24 @@ const MAX_SAFE_TIMEOUT_MS = 2_147_000_000; export function resolveLlmIdleTimeoutMs(params?: { cfg?: OpenClawConfig; trigger?: EmbeddedRunTrigger; + runTimeoutMs?: number; }): number { + const clampTimeoutMs = (valueMs: number) => Math.min(Math.floor(valueMs), MAX_SAFE_TIMEOUT_MS); const raw = params?.cfg?.agents?.defaults?.llm?.idleTimeoutSeconds; // 0 means explicitly disabled (no timeout). if (raw === 0) { return 0; } if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) { - return Math.min(Math.floor(raw) * 1000, MAX_SAFE_TIMEOUT_MS); + return clampTimeoutMs(raw * 1000); + } + + const runTimeoutMs = params?.runTimeoutMs; + if (typeof runTimeoutMs === "number" && Number.isFinite(runTimeoutMs) && runTimeoutMs > 0) { + if (runTimeoutMs >= MAX_SAFE_TIMEOUT_MS) { + return 0; + } + return clampTimeoutMs(runTimeoutMs); } const agentTimeoutSeconds = params?.cfg?.agents?.defaults?.timeoutSeconds; @@ -37,7 +47,7 @@ export function resolveLlmIdleTimeoutMs(params?: { Number.isFinite(agentTimeoutSeconds) && agentTimeoutSeconds > 0 ) { - return Math.min(Math.floor(agentTimeoutSeconds) * 1000, MAX_SAFE_TIMEOUT_MS); + return clampTimeoutMs(agentTimeoutSeconds * 1000); } if (params?.trigger === "cron") {