[BUG] Agentic loop terminates after clean tool_result on empty-text/empty-thinking tool_use turn — Windows v2.1.138, long post-tool inference
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
On Windows Claude Code v2.1.138 (Opus 4.7, 1M context), the agentic loop occasionally terminates immediately after a clean tool_result is delivered back to the model, even though the preceding assistant message had stop_reason: "tool_use". The harness emits a stop_hook_summary with preventedContinuation: false, returns to the input prompt, and requires the user to manually type "continue" to resume. No error, no retry, no API failure record.
This has happened across every implementation phase of a long project. I instrumented and analyzed it across 23 prior session JSONLs in this project, then deliberately reproduced it 4 times in a single session. The signature is identical every time:
On the assistant message immediately preceding the stop_hook_summary:
message.stop_reason: "tool_use"- exactly 1
tool_useblock (never parallel — 0/28 stops happened on a parallel-tool turn) content[].texttotal length = 0content[].thinkingtotal length = 0
On the stop_hook_summary entry that follows:
preventedContinuation: falsehookErrors: []level: "suggestion"hasOutput: false
This rules out user-side Stop hooks as the cause — the agentic loop terminates before the Stop hook runs.
A striking property is turn_duration on stopped turns. The 4 in-session reproductions had turn_duration of 76,752 / 84,446 / 109,600 / 217,978 ms for tiny tool calls (small file Reads, or a cargo Bash call). Normal completed turns in the same session run 2–8 seconds end-to-end. So roughly the entire turn duration is spent after the tool_result is delivered to the model, waiting for the post-tool-result inference call to return. The agentic loop ends with no further assistant message, suggesting the SSE stream for that inference call was aborted silently (consistent with the SDK-level root cause documented in #38905) and the loop interpreted the absent response as a clean turn end.
The rate is also bursty. Baseline across 23 prior sessions: 33 stops in 6,011 tool_use turns ≈ 0.55%. During the high-volume repro phase in this session: ~7.7% (2 stops in ~26 silent single-tool Reads). Suggests a per-session factor (cache state, server-side latency band, or context-size weighting) that shifts the rate from very-rare to frequent.
What Should Happen?
When stop_reason: "tool_use" was indicated on the prior turn and the tool_result is delivered cleanly, Claude Code should re-invoke the model with the tool_result. If the post-tool-result SSE stream aborts, the agent loop should detect the abort (missing message_stop event, stop_reason: null, or zero content blocks) and either retry the inference call or surface a visible "stream aborted, retrying…" message — not fire Stop hooks as if the turn completed normally.
Error Messages/Logs
No error messages are surfaced to the user. The defining property of this bug is that everything looks clean: no `isApiErrorMessage` record, no stderr, no failed-tool indicator. The JSONL sequence for each event looks like this:
[assistant] stop_reason=tool_use, content=[{type:tool_use, name:Read, input:{...}}], textLen=0, thinkingLen=0
[user] content=[{type:tool_result, tool_use_id:..., content:"..."}], interrupted=false
[system] subtype=stop_hook_summary, preventedContinuation=false, hookErrors=[], level=suggestion, hasOutput=false
[system] subtype=turn_duration, durationMs=217978, messageCount=833
[user] content="continue" ← manually typed by user OR replaced by an external schedule wakeup
Concrete instance (entry indices in `bb126c72-f8d3-4d50-9367-8df3ec07d018.jsonl`):
[802] assistant: stop_reason=tool_use, tools=[Read("edit_line_text.rs", offset=1, limit=15)], textLen=0, thinkingLen=0
[803] user: tool_result, len=777 bytes, interrupted=false
[804] system: stop_hook_summary, preventedContinuation=false, hookErrors=[], level=suggestion
[805] system: turn_duration, durationMs=217978, messageCount=833
Steps to Reproduce
The bug is stochastic at a low baseline rate (~0.5%) and bursty (~7%+ once a session hits it). To reproduce reliably you need volume.
- Start a fresh Claude Code session on Windows with Opus 4.7 (1M context).
- Ask Claude to read many small file ranges sequentially as single-tool turns (one
Readper assistant message, no parallel tools), e.g. "silently read 30 different small file ranges in this repo, one Read per message, no commentary, no thinking." This deliberately produces assistant turns matching the signature: 1 tool_use, textLen=0, thinkingLen=0. - Wait. Most calls complete in 2–8 seconds end-to-end. Occasionally one will hang for 60+ seconds (in my data: 77s, 84s, 110s, 218s) and then the agentic loop terminates instead of producing the next assistant message.
- The terminal returns to the
❯input prompt. The JSONL showsstop_hook_summarywithpreventedContinuation: falseright after thetool_result, thenturn_durationwith the elapsed milliseconds, then the cursor is awaiting user input. - User types literally anything ("continue", "go", etc.) and Claude resumes from the next logical step.
If you'd like a deterministic harness, I built one using /loop dynamic mode with a 180-second safety-net ScheduleWakeup. It armed a recovery wakeup before each batch of silent reads; when the bug fired mid-batch, the safety net rescued the loop on the next wake-up, and a scanner identified the new bug event from the JSONL without any user input. 2 out of 2 in-loop firings were auto-recovered this way — I can share the harness on request.
Session id with 4 clean reproductions: bb126c72-f8d3-4d50-9367-8df3ec07d018. JSONL path: ~/.claude/projects/D--PROJECTS-continuity/bb126c72-f8d3-4d50-9367-8df3ec07d018.jsonl. I can attach an excerpt showing all 4 events on request.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.140 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
Universal fingerprint (28/28 events: 4 this session + 24 historical across 23 sessions)
Every observed bug event shares the four-property assistant-turn signature listed in What's Wrong: stop_reason: tool_use, exactly 1 tool_use, textLen=0, thinkingLen=0. Tool mix: Read 21, Bash 10, MCP tool 1, Read 3 (in this session). The common factor is the assistant-turn shape, not the tool.
What this is NOT (eliminated from instrumentation)
- Not the user's Stop hook.
preventedContinuation: false,hookErrors: []. Hook runs after the agentic loop has already ended. - Not tool errors.
interrupted: false, noisErroron any tool_result. - Not parallel tools. 0/28 stopped turns had >1 tool_use block. Parallel-tool turns are immune in our data.
- Not long sessions only. This session's
messageCountat stop ranged from 132 → 845. Earlier "long session" correlation was a sampling artifact. - Not MCP plugin-induced. 0/28 stops happened on an MCP tool call in this session's high-volume harness; the only one was incidental in the 23-session historical baseline.
Relationship to existing issues
Same bug family as several existing issues, but a distinct variant:
- #38905 (closed as duplicate) — silent SSE stream abort. Root-cause analysis there identifies the SDK's SSE iterator silently swallowing
AbortError/FetchRequestCanceledException. That mechanism likely applies, but #38905 describes an abort on the initial inference stream; mine is on the post-tool-result inference call (consistent withtextLen=0 AND thinkingLen=0— the model never gets to start emitting any content blocks). - #47931 (Windows, open, stale, has-repro) — same platform, also silent terminate after tool_result, but in that case the Node process actually dies and the JSONL ends with no
stop_hook_summaryorturn_duration. Mine produces cleanstop_hook_summary+turn_durationrecords — the harness keeps running, only the agentic loop ends. - #40462 (macOS+vscode, open, stale) — "send any message to unstick" matches my "user types continue", but their UI shows
Thinking...indefinitely; mine ends the loop cleanly with a prompt. - #29881 (Linux, closed not-planned) — silent tool stop with Stop hook NOT fired. Mine fires the hook with
preventedContinuation: false, so that variant's fix wouldn't catch mine. - #13973 — "No response requested." Mine emits empty content, not the literal "No response requested." string.
Suggested fix direction
In the agent loop, after consuming the SSE stream for the post-tool-result inference call:
- If the prior turn had
stop_reason: "tool_use"AND the new assistant message has 0 content blocks (or only thinking with no text/tool_use), treat it as an aborted stream and retry the inference call. - At minimum, surface a visible "post-tool-result stream aborted, retrying…" message instead of silently ending the loop and firing Stop hooks.
Artifacts available on request
stop_log.json— chronological 4-event log with full tool inputs and per-eventturn_duration_ms.- Excerpt of
bb126c72-...jsonlshowing the exact[assistant tool_use] → [user tool_result] → [system stop_hook_summary preventedContinuation:false] → [system turn_duration]sequence for each of the 4 events. - The
/looprepro harness (deterministic scheduling + safety-netScheduleWakeup+ JSONL stop-detector) that auto-recovered 2/2 in-loop firings.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗