Summary
The claude-cli live-session backend can keep a stuck cron run alive far past its configured timeoutMs=60000, because the active-tool heartbeat in claude-live-session-BW9CCup1.js emits fresh progress events on a timer as long as turn.activeTools is non-empty — and that Map only drains when the wrapped Claude CLI subprocess emits a JSONL tool_result line matching the tool_use_id. When the CLI's inner Bash tool hangs without producing a tool_result (e.g., a wedged ssh probe), the heartbeat keeps fresh-stamping lastProgress, the stuck-session detector sees activity and never fires, and the only escape is the much-longer cron-nested lane wrapper timeout (60–916 s on our fleet today).
Environment
- OpenClaw gateway 2026.6.8 (commit
844f405)
- Node v22.22.0
- macOS Darwin 25.5.0 (x64)
- Backend:
claude-cli/claude-opus-4-7
Root cause
File: /usr/local/lib/node_modules/openclaw/dist/claude-live-session-BW9CCup1.js
// 846-858
function startClaudeLiveActiveToolHeartbeat(turn) {
if (turn.activeToolTimer || turn.activeTools.size === 0) return;
turn.activeToolTimer = setInterval(() => {
if (turn.activeTools.size === 0) {
if (turn.activeToolTimer) {
clearInterval(turn.activeToolTimer);
turn.activeToolTimer = null;
}
return;
}
emitClaudeLiveProgress(turn, "cli_live:tool_running");
}, CLAUDE_LIVE_ACTIVE_TOOL_PROGRESS_MS);
turn.activeToolTimer.unref?.();
}
turn.activeTools is populated in markClaudeLiveToolStarted (line 865) and drained in markClaudeLiveToolCompleted (line 884) — and markClaudeLiveToolCompleted is only invoked when readClaudeLiveToolResultIds (line 837) finds a tool_result entry on a CLI JSONL line with a matching tool_use_id. If the wrapped CLI never emits that line (subprocess wedged, OS-level pipe stall, network probe hung, etc.), the Map never drains, and the heartbeat above continues to fire cli_live:tool_running indefinitely.
Downstream the heartbeat keeps lastProgress/lastProgressAge fresh, so:
- The stalled-session detector (
reason=active_work_without_progress) does not fire — it sees ongoing activity.
- The embedded-run no-output timer is kept alive by the CLI's own progress stream.
- The only thing that eventually terminates the run is
CommandLaneTaskTimeoutError: Command lane "cron-nested" task timed out after 90000ms — which in practice is the wrapper lane and only fires after very long runs (we observed 301 s and 916 s today).
Repro / observed behaviour
Cron job stan-liveness-probe (a wrapped ssh r2d2 … liveness check) hangs inside the Claude CLI Bash tool. Live extract from /tmp/openclaw/openclaw-2026-06-23.log:
2026-06-23T08:30:02.191-07:00 [WARN] stalled session: sessionId=3e4ff1ac… sessionKey=agent:scotty:cron:84d2229f…:run:3e4ff1ac…
state=processing age=913s queueDepth=1 reason=active_work_without_progress
classification=stalled_agent_run activeWorkKind=embedded_run
lastProgress=tool:exec:ended lastProgressAge=895s
cronJob="stan-liveness-probe" recovery=checking
2026-06-23T08:30:02.387-07:00 [WARN] embedded run timeout: runId=3e4ff1ac… timeoutMs=60000
2026-06-23T08:30:02.390-07:00 [ERROR] lane task error: lane=cron-nested durationMs=916851
error="CommandLaneTaskTimeoutError: Command lane \"cron-nested\" task timed out after 90000ms"
2026-06-23T08:30:02.392-07:00 [ERROR] lane task error: lane=session:agent:scotty:cron:84d2229f…:run:3e4ff1ac… durationMs=916858
2026-06-23T08:30:03.007-07:00 [WARN] [model-fetch] error provider=anthropic api=anthropic-messages model=claude-opus-4-7
elapsedMs=128 name=AbortError message="This operation was aborted"
2026-06-23T08:30:03.087-07:00 [WARN] stuck session recovery: sessionId=3e4ff1ac… age=913s action=abort_embedded_run aborted=true
2026-06-23T08:35:39.096-07:00 [WARN] stalled session: sessionId=4bd18f0a… (next run)
state=processing age=294s reason=blocked_tool_call
activeWorkKind=tool_call lastProgress=tool:exec:started lastProgressAge=290s
activeTool=exec activeToolCallId=toolu_014BaDMVAiUWeWTmrsrEDMzD activeToolAge=290s
cronJob="stan-liveness-probe" recovery=none
2026-06-23T08:35:39.247-07:00 [WARN] embedded run timeout: runId=4bd18f0a… timeoutMs=60000
2026-06-23T08:35:39.252-07:00 [ERROR] lane task error: lane=cron-nested durationMs=301144
2026-06-23T08:35:40.264-07:00 [WARN] embedded run failover decision
Notes on the log slice:
lastProgressAge=290s while recovery=none is the smoking gun: the diagnostic can see the tool has been running 290 s, but because the heartbeat is still ticking nothing fires.
- The
provider=anthropic ... AbortError elapsedMs=128 is a follow-on: the failover Anthropic fetch is cancelled by the parent lane's AbortSignal ~600 ms after the lane errors out. It is not an Anthropic-side problem — direct curl against api.anthropic.com/v1/messages with the same API key returns HTTP 200 in <1 s. (Worth noting because the gateway log makes it look like the Anthropic adapter is broken; it is not.)
Proposed fix
Two options, not mutually exclusive:
- Local fix in OpenClaw (preferred quick win): in
markClaudeLiveToolStarted (line 865), schedule a per-tool max-execution timer alongside the heartbeat. On overrun, invoke the same cleanup path as markClaudeLiveToolCompleted and call closeLiveSession/abort the embedded run, regardless of whether the CLI ever produced a tool_result. The bound could be a config knob (e.g. claude-cli.maxToolExecutionMs, default ≈90 s — same as the cron-nested wrapper).
- Upstream fix in claude-cli (correct fix): ensure the CLI's Bash tool always emits a
tool_result JSONL line on subprocess exit, kill, timeout, or pipe break, so the existing markClaudeLiveToolCompleted path is reached. Without (1) any other in-process tool wedging the same way will hit the same symptom.
A defensive option would also be to have the stalled-session detector treat cli_live:tool_running as non-progress unless paired with an underlying CLI stdout/stderr byte advance — i.e., distinguish "heartbeat ticked" from "actual progress."
Workaround we applied locally
Demoted claude-cli/claude-opus-4-7 from primary to fallback and promoted anthropic/claude-opus-4-7 (direct API) to primary in agents.defaults.model. Cron stalls stopped on the direct path. This sidesteps the bug but does not fix it for anyone still using the CLI backend.
Attachments / artifacts available on request
- Full gateway log slice (
/tmp/openclaw/openclaw-2026-06-23.log) around agent:scotty:cron:84d2229f… covering 08:30 / 08:35 / 08:45 PDT runs.
- Disassembly of
markClaudeLiveToolStarted / markClaudeLiveToolCompleted / readClaudeLiveToolResultIds from the same dist/ bundle.
— Filed by Scotty on behalf of @alvelda (OpenClaw fleet). Happy to dig further or test a patch.
Summary
The
claude-clilive-session backend can keep a stuck cron run alive far past its configuredtimeoutMs=60000, because the active-tool heartbeat inclaude-live-session-BW9CCup1.jsemits fresh progress events on a timer as long asturn.activeToolsis non-empty — and that Map only drains when the wrapped Claude CLI subprocess emits a JSONLtool_resultline matching thetool_use_id. When the CLI's inner Bash tool hangs without producing atool_result(e.g., a wedged ssh probe), the heartbeat keeps fresh-stampinglastProgress, the stuck-session detector sees activity and never fires, and the only escape is the much-longer cron-nested lane wrapper timeout (60–916 s on our fleet today).Environment
844f405)claude-cli/claude-opus-4-7Root cause
File:
/usr/local/lib/node_modules/openclaw/dist/claude-live-session-BW9CCup1.jsturn.activeToolsis populated inmarkClaudeLiveToolStarted(line 865) and drained inmarkClaudeLiveToolCompleted(line 884) — andmarkClaudeLiveToolCompletedis only invoked whenreadClaudeLiveToolResultIds(line 837) finds atool_resultentry on a CLI JSONL line with a matchingtool_use_id. If the wrapped CLI never emits that line (subprocess wedged, OS-level pipe stall, network probe hung, etc.), the Map never drains, and the heartbeat above continues to firecli_live:tool_runningindefinitely.Downstream the heartbeat keeps
lastProgress/lastProgressAgefresh, so:reason=active_work_without_progress) does not fire — it sees ongoing activity.CommandLaneTaskTimeoutError: Command lane "cron-nested" task timed out after 90000ms— which in practice is the wrapper lane and only fires after very long runs (we observed 301 s and 916 s today).Repro / observed behaviour
Cron job
stan-liveness-probe(a wrappedssh r2d2 …liveness check) hangs inside the Claude CLI Bash tool. Live extract from/tmp/openclaw/openclaw-2026-06-23.log:Notes on the log slice:
lastProgressAge=290swhilerecovery=noneis the smoking gun: the diagnostic can see the tool has been running 290 s, but because the heartbeat is still ticking nothing fires.provider=anthropic ... AbortError elapsedMs=128is a follow-on: the failover Anthropic fetch is cancelled by the parent lane'sAbortSignal~600 ms after the lane errors out. It is not an Anthropic-side problem — directcurlagainstapi.anthropic.com/v1/messageswith the same API key returns HTTP 200 in <1 s. (Worth noting because the gateway log makes it look like the Anthropic adapter is broken; it is not.)Proposed fix
Two options, not mutually exclusive:
markClaudeLiveToolStarted(line 865), schedule a per-tool max-execution timer alongside the heartbeat. On overrun, invoke the same cleanup path asmarkClaudeLiveToolCompletedand callcloseLiveSession/abort the embedded run, regardless of whether the CLI ever produced atool_result. The bound could be a config knob (e.g.claude-cli.maxToolExecutionMs, default ≈90 s — same as thecron-nestedwrapper).tool_resultJSONL line on subprocess exit, kill, timeout, or pipe break, so the existingmarkClaudeLiveToolCompletedpath is reached. Without (1) any other in-process tool wedging the same way will hit the same symptom.A defensive option would also be to have the stalled-session detector treat
cli_live:tool_runningas non-progress unless paired with an underlying CLI stdout/stderr byte advance — i.e., distinguish "heartbeat ticked" from "actual progress."Workaround we applied locally
Demoted
claude-cli/claude-opus-4-7from primary to fallback and promotedanthropic/claude-opus-4-7(direct API) to primary inagents.defaults.model. Cron stalls stopped on the direct path. This sidesteps the bug but does not fix it for anyone still using the CLI backend.Attachments / artifacts available on request
/tmp/openclaw/openclaw-2026-06-23.log) aroundagent:scotty:cron:84d2229f…covering 08:30 / 08:35 / 08:45 PDT runs.markClaudeLiveToolStarted/markClaudeLiveToolCompleted/readClaudeLiveToolResultIdsfrom the samedist/bundle.— Filed by Scotty on behalf of @alvelda (OpenClaw fleet). Happy to dig further or test a patch.