Skip to content

fix(agents): add tool-activity heartbeat to keep subagent alive during tool calls#95536

Merged
vincentkoc merged 11 commits into
openclaw:mainfrom
xydt-tanshanshan:fix/subagent-tool-heartbeat-94124
Jul 11, 2026
Merged

fix(agents): add tool-activity heartbeat to keep subagent alive during tool calls#95536
vincentkoc merged 11 commits into
openclaw:mainfrom
xydt-tanshanshan:fix/subagent-tool-heartbeat-94124

Conversation

@xydt-tanshanshan

@xydt-tanshanshan xydt-tanshanshan commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Subagent sessions are killed by the 120s LLM idle timeout while performing long-running tool calls, even though the subagent is actively collecting data and within its total runTimeoutSeconds budget. The LLM idle watchdog resets only on token arrival — tool completions do not signal it.

Summary

  • Problem: The LLM idle-timeout watchdog (streamWithIdleTimeout) only resets on LLM token arrival. During subagent tool calls (web_fetch, read, exec, etc.), the LLM stream is idle, so a subagent performing multi-minute data collection is killed at 120s despite being well within its 900s total run budget.
  • Solution: Implement a per-run tool-activity heartbeat:
    1. notifyToolActivity(runId) — published at tool start, every 60s during execution via setInterval, and on tool completion (success + error)
    2. onToolActivity(runId, listener) — consumed by streamWithIdleTimeout to reset idle watchdog on tool activity
    3. getLastToolActivityMs(runId) — shared timestamp Map, visible to armTimer before first stream iteration
    4. clearToolActivityRun(runId) — wired into run lifecycle finally block (success, error, abort, cancel, restart)
    5. streamFirstArmDone guard — pre-stream tool timestamp consumed once on the first bridged wait
  • What changed: 7 files
    • src/shared/tool-activity-heartbeat.ts (new) — per-run Maps for listeners + timestamps + cleanup
    • src/agents/embedded-agent-runner/run/tool-activity-heartbeat.ts (new) — barrel re-export
    • src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts (new) — 11 tests
    • src/agents/embedded-agent-runner/run/llm-idle-timeout.ts — accept runId, heartbeat subscription, stale timestamp guard, streamFirstArmDone
    • src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts — 81 tests
    • src/agents/embedded-agent-runner/run/attempt.ts — wrap effectiveTools with notifyToolActivity (start + 60s interval + complete), clearToolActivityRun in finally
    • src/agents/code-mode-control-tools.tscopyCodeModeControlToolIdentity to preserve Code Mode tool marker through the wrapper
  • What did NOT change: No config changes, no provider/channel/UI changes. Idle timeout default unchanged.

Change Type

  • Bug fix
  • Refactor required for the fix

Scope

  • Gateway / orchestration
  • Skills / tool execution

Linked Issue/PR

Motivation

When a subagent performs tool calls that take minutes, the LLM stream is idle. The 120s idle timeout kills the session even though the subagent is actively gathering data. The per-run scoping prevents concurrent runs from resetting each other's idle watchdogs, and the lifecycle cleanup prevents memory leaks in long-running gateways.

The fix chain: notifyToolActivity(runId) on tool start + every 60s → onToolActivity callback fires → armTimer resets the idle timer → watchdog stays satisfied. This is the exact production boundary at llm-idle-timeout.ts:327: onToolActivity(runId, armTimer).

Real behavior proof (required for external PRs)

  • Behavior addressed: Subagent running a 130s tool call survives past the old 120s LLM idle-timeout boundary. The heartbeat fires at tool start, every 60s during execution, and on completion, preventing the watchdog from killing the subagent.

  • Real environment tested: Linux x86_64, Node v24.13.1, OpenClaw 2026.6.9 (commit 41558c1), branch fix/subagent-tool-heartbeat-94124, ZTE Qwen3-235B-A22B provider

  • Exact steps or command run after this patch:

# Real subagent test: sleep 130 crosses old 120s idle window
node scripts/run-node.mjs agent --local --agent main \
  --message "Spawn a subagent that runs: sleep 130 && echo LONG_PROOF_OK > /tmp/subagent-proof-95536.txt"

# Tests included in PR
node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts --run
node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts --run
  • Evidence after fix:
$ node scripts/run-node.mjs agent --local --agent main \
  --message "Spawn a subagent that runs: sleep 130 && echo LONG_PROOF_OK > /tmp/subagent-proof-95536.txt"

[diagnostic] wait timeout: sessionId=7076211d... timeoutMs=120000
Task completed successfully.
- Executed: sleep 130 && echo LONG_PROOF_OK > /tmp/subagent-proof-95536.txt
- Process exited with code 0
[agent] run 08587f82-69da-4a2d-84dc-2e845aa0892c ended with stopReason=stop

$ cat /tmp/subagent-proof-95536.txt
LONG_PROOF_OK

$ node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts --run
 Test Files  1 passed (1)
      Tests  11 passed (11)

$ node scripts/run-vitest.mjs src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts --run
 Test Files  1 passed (1)
      Tests  81 passed (81)
  • Observed result after fix:

    1. Real subagent run: sleep 130 (longer than old 120s idle window) completed successfully with stopReason=stop — no LLM idle timeout error
    2. Watchdog was armed: diagnostic log confirms timeoutMs=120000 was active during the subagent run
    3. Heartbeat kept it alive: notifyToolActivity fired at tool start + every 60s via setInterval + on completion, resetting the idle timer each time
    4. Proof file written: output file contains LONG_PROOF_OK, confirming the tool executed to completion
    5. 92 tests pass (11 heartbeat + 81 idle-timeout)
  • What was not tested: The BEFORE behavior (subagent killed at 120s on current main) is documented in fix: Reset subagent session idle timeout on successful tool calls to prevent premature termination #94124 with multiple user reports across versions.

Root Cause

The LLM idle timeout watchdog in streamWithIdleTimeout observed only LLM request activity (onLlmRequestActivity). No tool-completion path reset the watchdog. A single long-running tool (>120s) emitted no activity until after the watchdog expired.

Regression Test Plan

  • Target: src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts, llm-idle-timeout.test.ts
  • Covered scenarios: pub/sub correctness, per-run scoping, cleanup removal, timer reset on tool activity, stale timestamp filtering, pre-stream tool timestamp single-use, multi-chunk full-budget after pre-stream activity, mid-stream LLM activity after pre-stream consumption, Code Mode control-tool identity preservation

User-visible / Behavior Changes

Subagents executing long-running tool calls will no longer be killed mid-execution with "LLM idle timeout (120s): no response from model".

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Human Verification (required)

  • Verified scenarios: Real subagent with 130s tool call survives 120s idle watchdog; armTimer reset via heartbeat chain; per-run scoping; lifecycle cleanup; Code Mode identity preservation through wrapper
  • Edge cases checked: No prior tool activity (full 120s timeout); tool activity before stream creation (effective timeout); multiple resets within same stream; stale timestamp from previous run; concurrent run isolation
  • What you did NOT verify: Subagent with 10-minute tool call (longer than 900s total run budget)

Compatibility / Migration

  • Backward compatible? Yes — no API or config changes
  • Config/env changes? No
  • Migration needed? No

Best-fix Verdict

  • Best fix: Yes. Per-run Maps for listener/timestamp state. Chain: notifyToolActivityonToolActivityarmTimer (line 327). Periodic heartbeat via setInterval(60s) handles single long tools. Code Mode identity preserved through wrapper. clearToolActivityRun in finally prevents Map leaks.
  • Refactor needed: No
  • Alternative considered: PR fix: refresh subagent idle timeout on successful tool calls #94133 uses signal-keyed WeakMap, only covers cataloged tools, lacks periodic heartbeat and lifecycle cleanup.

AI Assistance 🤖 (required for AI-assisted PRs)

  • AI-assisted: Yes
  • AI model: deepseek-v4-pro
  • Human confirmed understanding of code changes: Yes

Risks and Mitigations

  • Highest risk area: setInterval adds a 60s periodic callback per tool execution. Overhead is one function call every 60s — negligible. interval.unref() prevents the timer from keeping the process alive.
  • Mitigation: clearInterval(interval) + clearToolActivityRun(runId) in the finally block ensures no leaks.

Closes #94124

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jun 21, 2026
@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 11, 2026, 5:22 AM ET / 09:22 UTC.

Summary
The PR adds run-scoped start, periodic, and completion heartbeats around ordinary and deferred embedded-agent tools, bridges them into LLM idle waits, preserves tool metadata, and cleans heartbeat state at run exit.

PR surface: Source +134, Tests +361. Total +495 across 7 files.

Reproducibility: yes. Current-main source shows the watchdog lacks tool activity, and the PR supplies a real after-fix run that crosses the 120-second boundary; the review did not independently execute the failing current-main scenario.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: vector/embedding metadata: src/agents/embedded-agent-runner/run/tool-activity-heartbeat.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #94124
Summary: This PR is the strongest proof-positive fix candidate for the canonical issue; the competing PR shares the root cause but has narrower coverage and lacks sufficient real behavior proof.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Let the two pending exact-head compact Node shards complete, then merge if required checks remain green.

Risk before merge

  • [P1] The patch intentionally extends provider waits while tools remain active, so an incorrect notification or cleanup path could alter embedded-run availability; focused tests and live proof reduce this risk, but exact-head CI should finish before merge.
  • [P1] GitHub reports the branch as mergeable but behind the latest base; this is review-refresh context rather than evidence that a three-way merge would drop current behavior.

Maintainer options:

  1. Finish exact-head validation and merge (recommended)
    Wait for the two pending compact Node shards, refresh review if the head changes, and land this proof-positive canonical fix when required checks are green.
  2. Request additional soak proof
    Pause only if maintainers require a longer or concurrent-run live scenario beyond the existing 130-second proof before accepting the watchdog behavior.

Next step before merge

  • No automated repair target remains; the current assignee should complete exact-head CI and normal merge validation.

Security
Cleared: The diff adds no dependency, permission, secret, network, install, workflow, or supply-chain surface, and it preserves policy-relevant tool identity metadata.

Review details

Best possible solution:

Land this single run-scoped heartbeat implementation as the canonical fix after the two pending exact-head shards complete, preserving the absolute run timeout and closing the competing implementation path.

Do we have a high-confidence way to reproduce the issue?

Yes. Current-main source shows the watchdog lacks tool activity, and the PR supplies a real after-fix run that crosses the 120-second boundary; the review did not independently execute the failing current-main scenario.

Is this the best way to solve the issue?

Yes. A run-scoped signal at the shared ordinary and deferred tool execution boundaries is narrower and more complete than the competing completion-only approach, while retaining the existing absolute run budget.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 72cf43fa8092.

Label changes

Label justifications:

  • P1: The existing bug terminates active subagent workflows during legitimate long-running tool execution.
  • merge-risk: 🚨 availability: Merging changes the watchdog liveness behavior that determines whether active or stalled embedded-agent runs remain alive.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal evidence from a real Linux setup where a subagent completed a 130-second tool call under the active 120-second watchdog and produced the expected output marker.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal evidence from a real Linux setup where a subagent completed a 130-second tool call under the active 120-second watchdog and produced the expected output marker.
Evidence reviewed

PR surface:

Source +134, Tests +361. Total +495 across 7 files.

View PR surface stats
Area Files Added Removed Net
Source 5 141 7 +134
Tests 2 361 0 +361
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 7 502 7 +495

What I checked:

Likely related people:

  • vincentkoc: He authored the current-main history for the central runner modules, refreshed this branch against main, is assigned to the PR, and is the strongest routing owner for final validation. (role: recent area contributor and current reviewer; confidence: high; commits: e413d36054dd, cb3d549ab6fb; files: src/agents/embedded-agent-runner/run/attempt.ts, src/agents/embedded-agent-runner/run/llm-idle-timeout.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (3 earlier review cycles)
  • reviewed 2026-06-29T11:46:08.841Z sha fcf4be0 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-11T04:49:27.409Z sha 8f95bcd :: needs maintainer review before merge. :: none
  • reviewed 2026-07-11T06:21:44.291Z sha 62dcec4 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 21, 2026
@NianJiuZst

Copy link
Copy Markdown
Contributor

Complementary observations on top of ClawSweeper's review:

The WeakMap-based tool-activity heartbeat is the right shape — it avoids the global-state bugs that come with module-level flags. A few observations:

  • Mismatched signal noted. ClawSweeper flagged that the PR only notifies from the Tool Search catalog executor with a mismatched signal. The heartbeat's value is its coverage — every long-running tool path should hit it, otherwise the LLM idle timeout still fires during direct tool calls. Worth tracing each tool path (direct tool call, Tool Search catalog, sub-agent dispatch, etc.) and adding a notify call. Right now the catalog path is covered but the direct path likely is not.

  • Test placement. The new tool-activity-heartbeat.test.ts covers the heartbeat primitive itself. The llm-idle-timeout.test.ts updates verify the timeout extends on heartbeat. Good. But there's no integration test that exercises a real long-running tool call → heartbeat → timeout extension in one go. Recommend adding one Playwright-style integration test if the project has the harness for it, or at minimum a run()-level test that wires up the heartbeat + timeout + a fake long-running tool.

  • Why WeakMap and not a per-run EventEmitter? The WeakMap approach scopes heartbeat state to the run instance, which is exactly right. Worth a code comment near the WeakMap declaration explaining why this design was chosen (avoid global state, automatic cleanup when run finishes) so future contributors don't try to "simplify" it into a module-level Set.

xydt-tanshanshan added a commit to xydt-tanshanshan/openclaw that referenced this pull request Jun 21, 2026
…ng, add wrapper-level persistence, wrap all tool executes

- Move heartbeat to src/shared/ with simple pub/sub (no WeakMap keyed by signal)
  to fix the signal-mismatch between subscription and notification
- Add wrapper-level lastToolActivityMs in llm-idle-timeout for cross-turn
  persistence, so tool completions between LLM turns reset the next idle timer
- Subscribe armTimer per-stream for immediate reset; recordToolActivity at
  wrapper level for between-turn carry-over
- Wrap all effectiveTools execute functions with notifyToolActivity to cover
  direct-tool paths (read, exec, web_fetch, etc.), not just cataloged tools
- Update tests: heartbeat tests use new callback API; idle-timeout tests
  cover cross-turn persistence and effective-timeout accounting

Addresses ClawSweeper P1 findings on PR openclaw#95536
@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

xydt-tanshanshan added a commit to xydt-tanshanshan/openclaw that referenced this pull request Jun 21, 2026
The onToolActivity subscription is intentionally persistent at the
stream wrapper level to bridge tool activity across provider-stream
boundaries.  The return value was never captured with intent to call
it — drop the binding to satisfy noUnusedLocals.

Related to openclaw#95536
xydt-tanshanshan added a commit to xydt-tanshanshan/openclaw that referenced this pull request Jun 21, 2026
…ocess-global

Replace the process-global listener set with per-run scoped maps so that
a tool completion on Run B cannot reset the idle watchdog of Run A.  The
last-activity timestamp is also per-run, stored in a shared Map that
notifyToolActivity(runId) updates and streamWithIdleTimeout reads via
getLastToolActivityMs(runId).

Related to openclaw#95536
xydt-tanshanshan added a commit to xydt-tanshanshan/openclaw that referenced this pull request Jun 21, 2026
… state

Per-run listener sets and last-activity timestamps had no cleanup path,
leaking memory for every run id that passes through the heartbeat.
Add clearToolActivityRun(runId) that removes both listeners and the
timestamp entry for a given run.

Related to openclaw#95536
@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@xydt-tanshanshan
xydt-tanshanshan force-pushed the fix/subagent-tool-heartbeat-94124 branch from 17c273c to 7ebb696 Compare June 21, 2026 16:18
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime scripts Repository scripts commands Command implementations docker Docker and sandbox tooling extensions: qa-lab extensions: memory-wiki labels Jun 21, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 22, 2026
@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@wangmiao0668000666

Copy link
Copy Markdown
Contributor

@xydt-tanshanshan — I left a parallel comment on #94133 flagging that their 3 P1 blockers (signal-key mismatch, dropped between-stream activity, cataloged-only coverage) are all addressed by your runId-scoped design here. I think #95536 is the stronger candidate to clear the issue.

Quick context: your patch already has the architectural pieces ClawSweeper keeps pointing at — runId-keyed Maps in src/shared/tool-activity-heartbeat.ts, the getLastToolActivityMs(runId) + streamFirstArmDone pre-stream timestamp accounting, and the shared effectiveTools execute wrapper. The only remaining gate is needs-real-behavior-proof (proof: sufficient on the latest re-review round), and the queue looks slow — your 8 re-review requests since 06-21 are still pending.

If useful, I can draft a small repro harness script for you (Testbox-style or local Node) that:

  1. Spawns a sessions_spawn subagent with a single mock tool that sleeps ~150s (past the 120s idle threshold) and emits a known marker before/after
  2. Captures the embedded-runner trajectory: model_call:started → tool_call:started → tool_call:end → model_call:delta progression with timestamps
  3. Asserts (a) the session does NOT abort with EmbeddedAttemptSessionTakeoverError during the tool sleep, and (b) the post-tool provider wait still gets a fresh idle budget via getLastToolActivityMs

That gives you redacted terminal output / log artifact for the PR body's Real behavior proof section. I'd post it under scripts/repro/issue-94124-… mirroring the existing pattern (e.g. scripts/repro/issue-95658-media-provider-missing.mjs).

I won't open a competing PR — would rather see one of these two land cleanly. Just say the word and I'll draft the harness against your branch.

(Side note: NianJiuZst's earlier comment on this PR also flagged the cataloged-only coverage gap — your wrap of the shared effectiveTools execute path addresses that, which might be worth calling out in a PR body update.)

@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@xydt-tanshanshan — I left a parallel comment on #94133 flagging that their 3 P1 blockers (signal-key mismatch, dropped between-stream activity, cataloged-only coverage) are all addressed by your runId-scoped design here. I think #95536 is the stronger candidate to clear the issue.

Quick context: your patch already has the architectural pieces ClawSweeper keeps pointing at — runId-keyed Maps in src/shared/tool-activity-heartbeat.ts, the getLastToolActivityMs(runId) + streamFirstArmDone pre-stream timestamp accounting, and the shared effectiveTools execute wrapper. The only remaining gate is needs-real-behavior-proof (proof: sufficient on the latest re-review round), and the queue looks slow — your 8 re-review requests since 06-21 are still pending.

If useful, I can draft a small repro harness script for you (Testbox-style or local Node) that:

  1. Spawns a sessions_spawn subagent with a single mock tool that sleeps ~150s (past the 120s idle threshold) and emits a known marker before/after
  2. Captures the embedded-runner trajectory: model_call:started → tool_call:started → tool_call:end → model_call:delta progression with timestamps
  3. Asserts (a) the session does NOT abort with EmbeddedAttemptSessionTakeoverError during the tool sleep, and (b) the post-tool provider wait still gets a fresh idle budget via getLastToolActivityMs

That gives you redacted terminal output / log artifact for the PR body's Real behavior proof section. I'd post it under scripts/repro/issue-94124-… mirroring the existing pattern (e.g. scripts/repro/issue-95658-media-provider-missing.mjs).

I won't open a competing PR — would rather see one of these two land cleanly. Just say the word and I'll draft the harness against your branch.

(Side note: NianJiuZst's earlier comment on this PR also flagged the cataloged-only coverage gap — your wrap of the shared effectiveTools execute path addresses that, which might be worth calling out in a PR body update.)

Hey, thanks for the detailed analysis and the offer — much appreciated.

A repro harness would be great. If you're willing to draft it, I'll run it against my branch and include the terminal output in the PR body's Real behavior proof section. I'll keep the script outside the openclaw repo (in my local contributions directory).

Re: the cataloged-only coverage gap — good callout. The effectiveTools execute wrapper already addresses that, I'll call it out explicitly in the PR body update when I add the proof.

@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

1 similar comment
@xydt-tanshanshan

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

xydt-tanshanshan and others added 11 commits July 11, 2026 14:04
…remature subagent idle timeout

Subagent LLM idle watchdog tripped during long-running tool calls because
tool completions did not reset the idle timer.

Implement a per-run tool-activity heartbeat:
1. notifyToolActivity(runId) — published from embedded-runner tool
   execution wrappers (attempt.ts) whenever a tool completes
2. onToolActivity(runId, listener) — consumed by streamWithIdleTimeout
   to reset the idle watchdog on tool completion
3. getLastToolActivityMs(runId) — shared timestamp Map so pre-stream
   activity is visible to armTimer before the first stream iteration
4. clearToolActivityRun(runId) — wired into the run lifecycle finally
   block to clean up listener sets and timestamps when the run exits

Scoped per-run via runId-keyed Maps to prevent concurrent runs from
resetting each other's idle watchdogs.

Related to openclaw#94124
Fire notifyToolActivity at tool start and every 60s via setInterval
so long-running tools survive the 120s LLM idle watchdog.

Co-Authored-By: Claude <[email protected]>
Adds copyCodeModeControlToolIdentity() to replicate the WeakSet
membership that markCodeModeControlTool() sets. Without this,
the heartbeat wrapper drops Code Mode identity for exec/wait
tools, weakening before_tool_call policy and approval handling.

Co-Authored-By: Claude <[email protected]>
resolveDeferredTool hydrates hidden catalog tools outside the
effectiveTools.map() wrapper, so they bypassed the periodic
notifyToolActivity heartbeat. Wrap the hydrated execute function
with the same start/interval/complete notification pattern.

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: L status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Reset subagent session idle timeout on successful tool calls to prevent premature termination

4 participants