Infinite retry loop when StreamIdleTimeoutError occurs during tool input generation #8573

Closed
opened 2026-02-16 18:10:18 -05:00 by yindo · 3 comments
Owner

Originally created by @dzianisv on GitHub (Feb 4, 2026).

Originally assigned to: @rekram1-node on GitHub.

Summary

When a model attempts to generate a large tool input (e.g., writing a full page of content), the stream can stall and trigger a StreamIdleTimeoutError. This error is marked as retryable, causing an infinite loop where the model repeatedly attempts the same failing operation.

Reproduction

  1. Ask the agent to write a large file (e.g., "create a full product page with multiple sections")
  2. The model starts generating a write tool call with large content parameter
  3. The API stalls during tool input generation (possibly due to rate limiting or output token limits)
  4. After 60 seconds, StreamIdleTimeoutError is thrown
  5. Error is retried with exponential backoff
  6. Model sees previous attempt failed with "Tool execution aborted"
  7. Model tries the exact same approach
  8. Loop continues indefinitely

Evidence from Logs

ERROR 2026-02-05T02:51:10 service=session.processor error=Stream idle timeout: no data received for 60000ms
ERROR 2026-02-05T02:52:16 service=session.processor error=Stream idle timeout: no data received for 60000ms
ERROR 2026-02-05T02:53:23 service=session.processor error=Stream idle timeout: no data received for 60000ms
ERROR 2026-02-05T02:54:34 service=session.processor error=Stream idle timeout: no data received for 60000ms

Task verification showed 10 consecutive write attempts with empty inputs:

## Tools Used
write: {}
write: {}
write: {}
write: {}
write: {}
write: {}
write: {}
write: {}
write: {}
write: {}

Root Cause Analysis

The Retry Loop

Model generates Write tool with large content
    ↓
API stalls during tool input JSON generation
    ↓
60 seconds pass with no stream chunks
    ↓
StreamIdleTimeoutError thrown (processor.ts:44)
    ↓
Converted to APIError with isRetryable: true (message-v2.ts:715)
    ↓
retry.ts.retryable() returns message string
    ↓
processor.ts catches, increments attempt, waits, continues (line 403-420)
    ↓
New LLM.stream() starts fresh
    ↓
Model sees "Tool execution aborted" error, tries same approach
    ↓
INFINITE LOOP

Why Doom Loop Detection Doesn't Trigger

The existing doom loop detection (processor.ts:207-232) checks:

if (part.state.status === "running" && part.state.input) {
  // Track same tool + same input called 3 times
}

But this fails because:

  1. Stream dies during tool-input-start phase (before tool-call)
  2. Tool never reaches "running" status
  3. Input is always {} (empty) - JSON never completed
  4. Cleanup marks tool as "error" with empty input
  5. Each retry has a different tool call ID, so not detected as duplicate

Suggested Fixes

Option 1: Add max retries for StreamIdleTimeoutError

// In processor.ts
let idleTimeoutRetries = 0
const MAX_IDLE_TIMEOUT_RETRIES = 3

// In catch block:
if (e instanceof StreamIdleTimeoutError) {
  idleTimeoutRetries++
  if (idleTimeoutRetries >= MAX_IDLE_TIMEOUT_RETRIES) {
    input.assistantMessage.error = MessageV2.fromError(
      new Error("Stream repeatedly timed out. The model may be trying to generate too much content at once."),
      { providerID: input.model.providerID }
    )
    return "stop"
  }
}

Option 2: Detect repeated incomplete tool calls

// Track tools that fail during input generation
const incompleteToolAttempts: Record<string, number> = {}

// When tool-input-start fires but stream dies before tool-call:
if (part.type === "tool" && Object.keys(part.state.input || {}).length === 0) {
  incompleteToolAttempts[part.tool] = (incompleteToolAttempts[part.tool] || 0) + 1
  if (incompleteToolAttempts[part.tool] >= DOOM_LOOP_THRESHOLD) {
    // Trigger doom loop - stop and surface error
  }
}

Option 3: Better error message to help model recover

Instead of generic "Tool execution aborted", provide actionable guidance:

"Tool execution aborted: stream timed out after 60s while generating tool input. 
This often happens when writing very large content. Try breaking the write into smaller chunks."

Environment

  • Provider: github-copilot
  • Model: claude-opus-4.5
  • Stream idle timeout: 60000ms (default)
  • Tool: write

Related Code

  • packages/opencode/src/session/processor.ts - Stream processing, idle timeout, doom loop detection
  • packages/opencode/src/session/message-v2.ts - StreamIdleTimeoutError class, error conversion
  • packages/opencode/src/session/retry.ts - Retry logic
  • packages/opencode/src/session/prompt.ts - Main agentic loop
Originally created by @dzianisv on GitHub (Feb 4, 2026). Originally assigned to: @rekram1-node on GitHub. ## Summary When a model attempts to generate a large tool input (e.g., writing a full page of content), the stream can stall and trigger a `StreamIdleTimeoutError`. This error is marked as retryable, causing an infinite loop where the model repeatedly attempts the same failing operation. ## Reproduction 1. Ask the agent to write a large file (e.g., "create a full product page with multiple sections") 2. The model starts generating a `write` tool call with large `content` parameter 3. The API stalls during tool input generation (possibly due to rate limiting or output token limits) 4. After 60 seconds, `StreamIdleTimeoutError` is thrown 5. Error is retried with exponential backoff 6. Model sees previous attempt failed with "Tool execution aborted" 7. Model tries the exact same approach 8. Loop continues indefinitely ## Evidence from Logs ``` ERROR 2026-02-05T02:51:10 service=session.processor error=Stream idle timeout: no data received for 60000ms ERROR 2026-02-05T02:52:16 service=session.processor error=Stream idle timeout: no data received for 60000ms ERROR 2026-02-05T02:53:23 service=session.processor error=Stream idle timeout: no data received for 60000ms ERROR 2026-02-05T02:54:34 service=session.processor error=Stream idle timeout: no data received for 60000ms ``` Task verification showed 10 consecutive write attempts with empty inputs: ``` ## Tools Used write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} ``` ## Root Cause Analysis ### The Retry Loop ``` Model generates Write tool with large content ↓ API stalls during tool input JSON generation ↓ 60 seconds pass with no stream chunks ↓ StreamIdleTimeoutError thrown (processor.ts:44) ↓ Converted to APIError with isRetryable: true (message-v2.ts:715) ↓ retry.ts.retryable() returns message string ↓ processor.ts catches, increments attempt, waits, continues (line 403-420) ↓ New LLM.stream() starts fresh ↓ Model sees "Tool execution aborted" error, tries same approach ↓ INFINITE LOOP ``` ### Why Doom Loop Detection Doesn't Trigger The existing doom loop detection (processor.ts:207-232) checks: ```typescript if (part.state.status === "running" && part.state.input) { // Track same tool + same input called 3 times } ``` But this fails because: 1. Stream dies during `tool-input-start` phase (before `tool-call`) 2. Tool never reaches "running" status 3. Input is always `{}` (empty) - JSON never completed 4. Cleanup marks tool as "error" with empty input 5. Each retry has a different tool call ID, so not detected as duplicate ## Suggested Fixes ### Option 1: Add max retries for StreamIdleTimeoutError ```typescript // In processor.ts let idleTimeoutRetries = 0 const MAX_IDLE_TIMEOUT_RETRIES = 3 // In catch block: if (e instanceof StreamIdleTimeoutError) { idleTimeoutRetries++ if (idleTimeoutRetries >= MAX_IDLE_TIMEOUT_RETRIES) { input.assistantMessage.error = MessageV2.fromError( new Error("Stream repeatedly timed out. The model may be trying to generate too much content at once."), { providerID: input.model.providerID } ) return "stop" } } ``` ### Option 2: Detect repeated incomplete tool calls ```typescript // Track tools that fail during input generation const incompleteToolAttempts: Record<string, number> = {} // When tool-input-start fires but stream dies before tool-call: if (part.type === "tool" && Object.keys(part.state.input || {}).length === 0) { incompleteToolAttempts[part.tool] = (incompleteToolAttempts[part.tool] || 0) + 1 if (incompleteToolAttempts[part.tool] >= DOOM_LOOP_THRESHOLD) { // Trigger doom loop - stop and surface error } } ``` ### Option 3: Better error message to help model recover Instead of generic "Tool execution aborted", provide actionable guidance: ``` "Tool execution aborted: stream timed out after 60s while generating tool input. This often happens when writing very large content. Try breaking the write into smaller chunks." ``` ## Environment - Provider: github-copilot - Model: claude-opus-4.5 - Stream idle timeout: 60000ms (default) - Tool: write ## Related Code - `packages/opencode/src/session/processor.ts` - Stream processing, idle timeout, doom loop detection - `packages/opencode/src/session/message-v2.ts` - StreamIdleTimeoutError class, error conversion - `packages/opencode/src/session/retry.ts` - Retry logic - `packages/opencode/src/session/prompt.ts` - Main agentic loop
yindo added the perf label 2026-02-16 18:10:18 -05:00
yindo closed this issue 2026-02-16 18:10:18 -05:00
Author
Owner

@dzianisv commented on GitHub (Feb 4, 2026):

Closing to reopen with more detailed logs and evidence.

@dzianisv commented on GitHub (Feb 4, 2026): Closing to reopen with more detailed logs and evidence.
Author
Owner

@github-actions[bot] commented on GitHub (Feb 5, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #11647: Sessions stalling out indefinitely due to a "provider error" - similar infinite retry loops with high retry counters
  • #11863: Web UI infinite processing loop when client/server clocks diverge - infinite loop caused by message handling logic
  • #11865: Tasks/Subagents getting stuck with no timeout/retry causing indefinite hangs
  • #11630: Write tool JSON parsing error with large files - same write tool failure scenario
  • #11531: JSON Parse Error with large content (>4KB) - workaround was splitting into smaller chunks
  • #11625: OpenCode stops responding to messages indefinitely
  • #12229: Stdio handshake timeout with 60s timeout similar to stream idle timeout

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Feb 5, 2026): This issue might be a duplicate of existing issues. Please check: - #11647: Sessions stalling out indefinitely due to a "provider error" - similar infinite retry loops with high retry counters - #11863: Web UI infinite processing loop when client/server clocks diverge - infinite loop caused by message handling logic - #11865: Tasks/Subagents getting stuck with no timeout/retry causing indefinite hangs - #11630: Write tool JSON parsing error with large files - same write tool failure scenario - #11531: JSON Parse Error with large content (>4KB) - workaround was splitting into smaller chunks - #11625: OpenCode stops responding to messages indefinitely - #12229: Stdio handshake timeout with 60s timeout similar to stream idle timeout Feel free to ignore if none of these address your specific case.
Author
Owner

@cjmayoral commented on GitHub (Feb 11, 2026):

I’m seeing the same pattern on Windows (OpenCode 1.1.49) with session ses_3b7971163ffexUgX5mjSgXDfWQ:

  • A tool call (edit) is recorded with empty input {} and error: "Tool execution aborted".
  • The assistant message that produced it ended with finish: "length" (output limit), suggesting the tool call was truncated before tool arguments were fully emitted.
  • This matches the description of stream dying during tool input generation; the tool input never completes, leaving {}.

Sanitized snippets available:

  • Tool error with empty input
  • Message finish: "length"
  • Session creation + model info

Hypothesis: when finish: "length" happens mid tool-input, the runtime still attempts tool execution with {}, which then fails. A guard that detects incomplete tool payloads (or retries to complete tool args) would avoid this class of “Tool execution aborted”.

If helpful, I can share the exact snippet JSONs.

@cjmayoral commented on GitHub (Feb 11, 2026): I’m seeing the same pattern on Windows (OpenCode 1.1.49) with session `ses_3b7971163ffexUgX5mjSgXDfWQ`: - A tool call (`edit`) is recorded with empty input `{}` and `error: "Tool execution aborted"`. - The assistant message that produced it ended with `finish: "length"` (output limit), suggesting the tool call was truncated before tool arguments were fully emitted. - This matches the description of stream dying during tool input generation; the tool input never completes, leaving `{}`. Sanitized snippets available: - Tool error with empty input - Message `finish: "length"` - Session creation + model info Hypothesis: when `finish: "length"` happens mid tool-input, the runtime still attempts tool execution with `{}`, which then fails. A guard that detects incomplete tool payloads (or retries to complete tool args) would avoid this class of “Tool execution aborted”. If helpful, I can share the exact snippet JSONs.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8573