fix: refresh subagent idle timeout on successful tool calls#94133
fix: refresh subagent idle timeout on successful tool calls#94133kumaxs wants to merge 1 commit into
Conversation
When a subagent performs long-running tool calls (e.g. web_fetch, read, exec), the LLM stream is idle. If no LLM token arrives within 120 seconds, the session is terminated with LLM idle timeout. This fix adds a tool-activity heartbeat mechanism: - A new module tool-activity-heartbeat.ts provides a subscription/ notification pair (onToolActivity / notifyToolActivity) modelled on the existing llm-request-activity pattern. - streamWithIdleTimeout (llm-idle-timeout.ts) now subscribes to both LLM-request activity AND tool-activity, resetting the idle timer via armTimer() on either signal. - attempt.ts calls notifyToolActivity(runAbortController.signal) after every completed tool call (both success and failure branches), so the idle watchdog sees activity even when the model is silent. The 30-second heartbeat window (TOOL_ACTIVITY_HEARTBEAT_MS = 30_000) is intentionally shorter than the default 120s idle timeout, giving slow tool calls multiple chances to refresh before the watchdog fires. Fixes openclaw#94124
|
@clawsweeper review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Codex review: needs real behavior proof before merge. Reviewed July 1, 2026, 3:31 PM ET / 19:31 UTC. Summary PR surface: Source +61. Total +61 across 3 files. Reproducibility: yes. for the PR defects at source level: the signal-key mismatch, dropped between-wait activity, and missed direct-tool path are visible from the current code and PR diff. I did not run a live subagent timeout reproduction in this read-only review. Review metrics: none identified. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land #95536 or rework this branch to the same run-scoped shared tool-execution heartbeat with focused tests and real subagent proof; keep #94124 open until a fix merges. Do we have a high-confidence way to reproduce the issue? Yes, for the PR defects at source level: the signal-key mismatch, dropped between-wait activity, and missed direct-tool path are visible from the current code and PR diff. I did not run a live subagent timeout reproduction in this read-only review. Is this the best way to solve the issue? No. A heartbeat is the right direction, but this implementation is not the best fix because it is keyed to the wrong lifecycle object and does not cover the shared tool execution boundary. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 24d94a54a516. Label changesLabel justifications:
Evidence reviewedPR surface: Source +61. Total +61 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Hey @kumaxs — I dug into the ClawSweeper review and wanted to flag the three P1 findings with concrete pointers, plus how #95536 already addresses each one. Not opening a competing PR; just trying to help the canonical issue get unblocked whichever PR lands. The 3 P1 findings, with the exact mismatch1. Signal-key mismatch (confidence 0.95) — current code is a no-op // src/agents/embedded-agent-runner/run/llm-idle-timeout.ts:299
const unsubscribeToolActivity = onToolActivity(streamAbortController.signal, armTimer);
// src/agents/embedded-agent-runner/run/attempt.ts:3678
notifyToolActivity(runAbortController.signal);
const toolActivityListeners = new WeakMap<AbortSignal, Set<() => void>>();So 2. Activity dropped between stream waits (confidence 0.88)
So even if the signal keys matched, the notification would still miss because the listener has already been torn down by the time the tool completes. 3. Cataloged-tools only (confidence 0.84) The two How #95536 addresses all threeFor awareness (so it's clear there's already a working shape on the table):
So #95536 already has the architectural pieces; it just needs Paths forward (your call)A few options I see, roughly ordered by blast radius:
Whatever you pick, I'd suggest pinging ClawSweeper once the patch is updated so the re-review queue picks it up — the If it's useful, I can draft a small repro harness script that exercises a subagent with a 3-minute mock tool call and shows |
|
This pull request has been automatically marked as stale due to inactivity. |
|
Closing due to inactivity. |
Problem
When a subagent (
sessions_spawnwithmode: "run") performs long-running tool calls (web_fetch,read,exec, etc.), the LLM stream is idle. If no LLM token arrives within 120 seconds, the session is terminated with:This is particularly impactful for research/analysis subagents that perform multi-step data collection before generating their final report. The subagent may have completed all its data gathering and be about to synthesize the output, but the 120s window expires and kills the session.
agents.defaults.subagents.runTimeoutSeconds(default 900s / 15min) only controls total wall-clock time, not idle time between LLM turns — so a subagent can be well within its total budget but still get killed because no LLM output happened during tool calls.Fix
A tool-activity heartbeat mechanism that keeps the session alive when tools are actively returning results:
tool-activity-heartbeat.ts— providesonToolActivity()/notifyToolActivity()modelled on the existingllm-request-activity.tspatternllm-idle-timeout.ts—streamWithIdleTimeoutnow subscribes to both LLM-request activity AND tool-activity viaarmTimer(), so either signal resets the idle watchdogattempt.ts— callsnotifyToolActivity(runAbortController.signal)after every completed tool call (both success and error branches)The 30-second heartbeat window is intentionally shorter than the 120s default idle timeout, giving slow tool calls multiple chances to refresh before the watchdog fires.
How it works
notifyToolActivity(runAbortController.signal)fires → idle timer resets (30s window)next()resets the timer via existingonLlmRequestActivityNo new config fields. No breaking changes. Just a smarter timeout origin.
Related