agents: honor explicit run timeout for LLM idle watchdog

This commit is contained in:
ImLukeF
2026-04-11 16:23:45 +10:00
parent 5605c89cb3
commit 7f2814fc4a
3 changed files with 32 additions and 2 deletions

View File

@@ -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(

View File

@@ -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 } } },

View File

@@ -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") {