fix(cli): prevent agent-loop freeze when a provider stalls after headers - #12588
Merged
Conversation
A provider can accept a request, return response headers, and then never send a byte of body data. The connection-phase request timeout was cleared as soon as headers arrived, so nothing bounded that wait and the agent turn hung indefinitely after a tool call completed: step-finish:tool-calls was recorded and the next step-start never arrived, with the HTTP server still responsive. Extend the same configured timeout deadline to the wait for the response body's first byte. The connection-phase timer covers the fetch up to headers; once headers arrive, the remaining deadline is handed to a first-byte guard that aborts the request if no data arrives. After the first byte the guard becomes a passthrough, so idle gaps inside an already streaming response (reasoning, buffering, slow token generation) are never touched and remain opt-in via chunkTimeout. This is a transport-level signal (bytes on the wire, before any content) rather than the absence of normalized AI SDK events, so it cannot fire on long prompt processing or reasoning the way the reverted stream watchdog did. timeout: false still disables the bound entirely. Adds a hermetic regression test that injects a simulated stalled socket through the provider's own fetch option via the plugin config hook, so the SDK, Kilo's fetch wrapper, SSE parsing, the processor and the agent loop all stay production code. The stalled request is transient, so the test asserts the turn recovers by retrying and completing instead of freezing. The test goes red without the fix (no retry, frozen at step-finish) and green with it. Refs #8656
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (8 files)
Reviewed by claude-sonnet-5 · Input: 38 · Output: 15K · Cached: 1.2M Review guidance: REVIEW.md from base branch |
marius-kilocode
enabled auto-merge
July 28, 2026 09:00
chrarnoldus
approved these changes
Jul 28, 2026
This was referenced Jul 29, 2026
This was referenced Jul 31, 2026
t7tran
pushed a commit
to t7tran/kilocode
that referenced
this pull request
Aug 14, 2026
…te-timeout fix(cli): prevent agent-loop freeze when a provider stalls after headers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
The agent turn no longer hangs indefinitely when a provider accepts a request, returns response headers, and then never sends a byte of body data. Previously
step-finish:tool-callswas recorded and the nextstep-startnever arrived, the session stayed busy with no error, and the HTTP server kept responding — the freeze reported in #8656.That issue is not MCP-specific (the reporter reproduced it with plain bash tool calls and no MCP); it is a transport-level gap in Kilo's provider fetch wrapper.
Root cause
Kilo already had a default 5-minute request timeout (
REQUEST_TIMEOUT_MS), but the fetch wrapper cleared it the moment response headers arrived. The post-header, pre-first-byte window was therefore unbounded by default: a response whose headers landed but whose body never produced data could sit forever, leaving the agent loop frozen between a finished tool call and the next LLM step.Change
Extend the same configured
timeoutdeadline to the wait for the response body's first byte.wrapFirstByte, which aborts the request if no body data arrives in time.chunkTimeout.timeout: falsestill disables the bound entirely.The signal is bytes on the wire before any content, not the absence of normalized AI SDK events, so it cannot fire on long prompt processing or reasoning the way the reverted stream watchdog did — see #12249 and its revert #12497. The implementation lives in the Kilo-owned provider mirror; the shared
provider.tschange is a minimal marked hook.requestTimeoutnow falls back to the default for invalid/unset values so the wait is always bounded; onlytimeout: falsedisables it.Test
The regression test injects a simulated stalled socket as the provider's own
fetchthrough the pluginconfighook (the supported injection point:provider.tsloads plugins before readingcfg.provider), so the openai-compatible SDK, Kilo's fetch wrapper, SSE parsing, the processor and the agent loop all stay production code. Nothing global is patched and no process env is mutated.The stalled request is transient, so the test asserts the outcome a user cares about: the turn recovers by retrying and completing instead of freezing. Timeline with the fix:
```
text | step-start | tool:bash:completed | step-finish:tool-calls | step-start | text | step-finish:stop
```
The second
step-startis what #8656 reports as never arriving.Red without the fix, green with it:
A second test keeps the raw freeze reachable through the documented
timeout: falseopt-out and asserts the server stays responsive during the hang, with explicit abort-and-wait-for-idle teardown.Scope
This bounds one class of agent-turn freeze: a provider response that sends headers and no body data. It does not cover stalls where the provider sends keepalives or SSE comments and then goes silent (those need
chunkTimeout), nor non-transport causes.#8656 is referenced as a matching symptom rather than closed by this change; confirming it as the reported cause needs either reporter validation on this build or transport logs showing the affected request received headers but no body data.