diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index 3a9523385b36..ba35ceb7b135 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -167,7 +167,7 @@ surfaces, while Codex native hooks remain a separate lower-level Codex mechanism - Agent runtime: `agents.defaults.timeoutSeconds` default 172800s (48 hours); enforced in `runEmbeddedAgent` abort timer. - Cron runtime: isolated agent-turn `timeoutSeconds` is owned by cron. The scheduler starts that timer when execution begins, aborts the underlying run at the configured deadline, then runs bounded cleanup before recording the timeout so a stale child session cannot keep the lane stuck. - Session liveness diagnostics: with diagnostics enabled, `diagnostics.stuckSessionWarnMs` classifies long `processing` sessions that have no observed reply, tool, status, block, or ACP progress. Active embedded runs, model calls, and tool calls report as `session.long_running`; owned silent model calls also stay `session.long_running` until `diagnostics.stuckSessionAbortMs` so slow or non-streaming providers are not reported as stalled too early. Active work with no recent progress reports as `session.stalled`; owned model calls switch to `session.stalled` at or after the abort threshold, and ownerless stale model/tool activity is not hidden as long-running. `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after `diagnostics.stuckSessionAbortMs` (default: at least 5 minutes and 3x the warning threshold) so queued work can resume without cutting off merely slow runs. Recovery emits structured requested/completed outcomes, and diagnostic state is marked idle only if the same processing generation is still current. Repeated `session.stuck` diagnostics back off while the session remains unchanged. -- Model idle timeout: OpenClaw aborts a model request when no response chunks arrive before the idle window. `models.providers..timeoutSeconds` extends this idle watchdog for slow local/self-hosted providers, but it is still bounded by any lower `agents.defaults.timeoutSeconds` or run-specific timeout because those control the whole agent run. Otherwise OpenClaw uses `agents.defaults.timeoutSeconds` when configured, capped at 120s by default. Cron-triggered runs with no explicit model or agent timeout disable the idle watchdog and rely on the cron outer timeout. +- Model idle timeout: OpenClaw aborts a model request when no response chunks arrive before the idle window. `models.providers..timeoutSeconds` extends this idle watchdog for slow local/self-hosted providers, but it is still bounded by any lower `agents.defaults.timeoutSeconds` or run-specific timeout because those control the whole agent run. Otherwise OpenClaw uses `agents.defaults.timeoutSeconds` when configured, capped at 120s by default. Cron-triggered cloud model runs with no explicit model or agent timeout use the same default idle watchdog; cron-triggered local or self-hosted model runs disable the implicit watchdog unless an explicit timeout is configured, so slow local providers should set `models.providers..timeoutSeconds`. - Provider HTTP request timeout: `models.providers..timeoutSeconds` applies to that provider's model HTTP fetches, including connect, headers, body, SDK request timeout, total guarded-fetch abort handling, and model stream idle watchdog. Use this for slow local/self-hosted providers such as Ollama before raising the whole agent runtime timeout, and keep the agent/runtime timeout at least as high when the model request needs to run longer. ## Where things can end early diff --git a/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts b/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts index e210012173fe..6459a6365a98 100644 --- a/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts +++ b/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts @@ -9,10 +9,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { notifyLlmRequestActivity } from "../../../shared/llm-request-activity.js"; import type { StreamFn } from "../../runtime/index.js"; -import { - resolveLlmIdleTimeoutMs, - streamWithIdleTimeout, -} from "./llm-idle-timeout.js"; +import { resolveLlmIdleTimeoutMs, streamWithIdleTimeout } from "./llm-idle-timeout.js"; const DEFAULT_LLM_IDLE_TIMEOUT_MS = 120_000; @@ -145,11 +142,11 @@ describe("resolveLlmIdleTimeoutMs", () => { ); }); - it("disables the default idle timeout for cron when no timeout is configured", () => { - expect(resolveLlmIdleTimeoutMs({ trigger: "cron" })).toBe(0); + it("uses the default idle timeout for cron cloud model calls when no timeout is configured", () => { + expect(resolveLlmIdleTimeoutMs({ trigger: "cron" })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS); const cfg = { agents: { defaults: {} } } as OpenClawConfig; - expect(resolveLlmIdleTimeoutMs({ cfg, trigger: "cron" })).toBe(0); + expect(resolveLlmIdleTimeoutMs({ cfg, trigger: "cron" })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS); }); it("caps agents.defaults.timeoutSeconds for cron before disabling the default idle timeout", () => { @@ -157,6 +154,15 @@ describe("resolveLlmIdleTimeoutMs", () => { expect(resolveLlmIdleTimeoutMs({ cfg, trigger: "cron" })).toBe(DEFAULT_LLM_IDLE_TIMEOUT_MS); }); + it("keeps cron local provider model calls opted out of the implicit idle watchdog", () => { + expect( + resolveLlmIdleTimeoutMs({ + trigger: "cron", + model: { baseUrl: "http://127.0.0.1:11434" }, + }), + ).toBe(0); + }); + it.each([ "http://localhost:11434", "http://127.0.0.1:11434", diff --git a/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts b/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts index ba6454cfa430..212eae429d21 100644 --- a/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts +++ b/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts @@ -183,10 +183,6 @@ export function resolveLlmIdleTimeoutMs(params?: { return clampImplicitTimeoutMs(agentTimeoutMs); } - if (params?.trigger === "cron") { - return 0; - } - // The default watchdog is a network-silence-as-hang guard for cloud providers. // Local providers can legitimately stream nothing for many minutes during // prompt evaluation or thinking, so falling back to the default would abort