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

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

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

Originally assigned to: @thdxr 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 with exponential backoff delays.

User Experience: The UI shows "Preparing write..." indefinitely, with the agent stuck in a loop. The only way to exit is to manually abort (Escape key).

Detailed Timeline from Real Session

Session ses_3d5454748ffeA0QZlbOzaY4s4q on project VibeBrowserProductPage:

Time Event Delay Since Last
02:50:05 Stream started (claude-opus-4.5) -
02:51:10 StreamIdleTimeoutError (60s timeout) 65s
02:51:12 Retry #1 started 2s backoff
02:52:16 StreamIdleTimeoutError 64s
02:52:20 Retry #2 started 4s backoff
02:53:23 StreamIdleTimeoutError 63s
02:53:31 Retry #3 started 8s backoff
02:54:34 StreamIdleTimeoutError 63s
02:54:50 Retry #4 started 16s backoff
02:55:53 StreamIdleTimeoutError 63s
02:56:23 Retry #5 started 30s backoff
02:57:27 StreamIdleTimeoutError 64s
02:57:52 User manually aborted -

Total time stuck: ~8 minutes before user intervention.

Raw Log Evidence

StreamIdleTimeoutError Sequence

ERROR 2026-02-05T02:51:10 +60107ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process
ERROR 2026-02-05T02:52:16 +60096ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process
ERROR 2026-02-05T02:53:23 +60218ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process
ERROR 2026-02-05T02:54:34 +60211ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process
ERROR 2026-02-05T02:55:53 +60214ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process
ERROR 2026-02-05T02:57:27 +60133ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n    at <anonymous> (src/session/processor.ts:44:20)" process

Retry Pattern with Exponential Backoff

INFO  2026-02-05T02:50:05 service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream
INFO  2026-02-05T02:51:12 +2002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream
INFO  2026-02-05T02:52:20 +4002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream
INFO  2026-02-05T02:53:31 +8003ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream
INFO  2026-02-05T02:54:50 +16003ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream
INFO  2026-02-05T02:56:23 +30002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream

Note the delays: 2s → 4s → 8s → 16s → 30s (capped at RETRY_MAX_DELAY_NO_HEADERS)

Task Verification Shows 10 Empty Write Attempts

From the reflection/task verification system:

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

## Agent's Response
You're right, I was overthinking. Let me just write the full page:

The Write tool was called 10 times with empty input {} because the stream died during tool-input-start phase before the JSON input was fully parsed.

Root Cause Analysis

The Retry Loop Flow

User asks: "continue working on full product page"
    ↓
Model starts generating Write tool call with large content
    ↓
Provider API stalls (rate limit, internal processing, or output token exhaustion)
    ↓
60 seconds pass with no stream data chunks
    ↓
StreamIdleTimeoutError thrown (processor.ts:44)
    ↓
Error converted to APIError with isRetryable: true (message-v2.ts:715)
    ↓
retry.ts.retryable() returns message string (line 62-64)
    ↓
processor.ts catches error, increments attempt, waits with backoff (line 403-420)
    ↓
New LLM.stream() call starts from scratch
    ↓
Model sees previous failed attempt with "Tool execution aborted" error
    ↓
Model tries THE SAME approach again
    ↓
REPEAT FOREVER (or until user aborts)

Why Doom Loop Detection Doesn't Trigger

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

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

This fails because:

  1. Stream dies during tool-input-start phase (before tool-call event)
  2. Tool never reaches "running" status - it stays in "pending"
  3. Input is always {} (empty) - JSON was never fully received
  4. Cleanup marks tool as "error" with empty input
  5. Each retry has a different tool call ID
  6. Empty inputs {} are not detected as "same input"

Code Path Evidence

message-v2.ts:711-720 - StreamIdleTimeoutError marked as retryable:

case e instanceof StreamIdleTimeoutError:
  return new MessageV2.APIError(
    {
      message: e.message,
      isRetryable: true,  // <-- This causes infinite retries
      metadata: {
        timeoutMs: String(e.timeoutMs),
      },
    },
    { cause: e },
  ).toObject()

processor.ts:403-420 - Retry logic with no max attempts:

} catch (e: any) {
  log.error("process", { error: e, stack: JSON.stringify(e.stack) })
  const error = MessageV2.fromError(e, { providerID: input.model.providerID })
  const retry = SessionRetry.retryable(error)
  if (retry !== undefined) {
    attempt++
    const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined)
    SessionStatus.set(input.sessionID, {
      type: "retry",
      attempt,
      message: retry,
      next: Date.now() + delay,
    })
    await SessionRetry.sleep(delay, input.abort).catch(() => {})
    continue  // <-- No max retry check for StreamIdleTimeoutError
  }
  // ...
}

processor.ts:442-458 - Cleanup marks incomplete tools as aborted:

for (const part of p) {
  if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
    await Session.updatePart({
      ...part,
      state: {
        ...part.state,
        status: "error",
        error: "Tool execution aborted",  // <-- Generic message, no actionable guidance
        // ...
      },
    })
  }
}

Environment

  • Provider: github-copilot
  • Model: claude-opus-4.5
  • Stream idle timeout: 60000ms (default)
  • Tool: write
  • User task: "continue working on full product page, target financial sectors"

Suggested Fixes

Option 1: Add max retries for StreamIdleTimeoutError (Recommended)

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

// In catch block, before retry logic:
if (e instanceof StreamIdleTimeoutError) {
  idleTimeoutRetries++
  if (idleTimeoutRetries >= MAX_IDLE_TIMEOUT_RETRIES) {
    input.assistantMessage.error = MessageV2.fromError(
      new Error(`Stream repeatedly timed out (${MAX_IDLE_TIMEOUT_RETRIES} attempts). The model may be trying to generate content that exceeds output limits. Try breaking the task into smaller pieces.`),
      { providerID: input.model.providerID }
    )
    Bus.publish(Session.Event.Error, {
      sessionID: input.assistantMessage.sessionID,
      error: input.assistantMessage.error,
    })
    break // Exit the retry loop
  }
}

Option 2: Detect repeated incomplete tool calls

Track tools that fail during input generation (empty inputs):

// In processor.ts
const incompleteToolAttempts: Record<string, number> = {}

// In cleanup section (line 442-458):
for (const part of p) {
  if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
    // Track incomplete tool attempts
    const inputSize = JSON.stringify(part.state.input || {}).length
    if (inputSize <= 2) { // Empty object "{}"
      incompleteToolAttempts[part.tool] = (incompleteToolAttempts[part.tool] || 0) + 1
      if (incompleteToolAttempts[part.tool] >= DOOM_LOOP_THRESHOLD) {
        blocked = true
        // Add guidance to error message
      }
    }
    // ... rest of cleanup
  }
}

Option 3: Better error message with actionable guidance

Instead of generic "Tool execution aborted":

error: `Tool execution aborted: stream timed out after ${timeoutMs/1000}s while generating tool input. This often happens when attempting to write very large content. Consider breaking the write operation into smaller chunks.`

Option 4: Make StreamIdleTimeoutError non-retryable (simplest)

// In message-v2.ts:711-720
case e instanceof StreamIdleTimeoutError:
  return new MessageV2.APIError(
    {
      message: e.message,
      isRetryable: false,  // <-- Stop automatic retries
      metadata: {
        timeoutMs: String(e.timeoutMs),
      },
    },
    { cause: e },
  ).toObject()

This surfaces the error to the user immediately, who can then choose to retry or modify their request.

Related Files

  • packages/opencode/src/session/processor.ts - Stream processing, idle timeout, doom loop detection, cleanup
  • packages/opencode/src/session/message-v2.ts - StreamIdleTimeoutError class, error conversion, isRetryable flag
  • packages/opencode/src/session/retry.ts - Retry logic, backoff calculation
  • packages/opencode/src/session/prompt.ts - Main agentic loop

Additional Context

This issue can occur with any provider when:

  1. The model tries to generate very large tool inputs (like writing full files)
  2. The provider has internal rate limiting or processing delays
  3. The model hits output token limits during tool input generation
  4. Network issues cause intermittent stalls

The exponential backoff makes this particularly frustrating - after a few retries, the user is waiting 30+ seconds between each failed attempt, with no indication that the same error will keep occurring.

Originally created by @dzianisv on GitHub (Feb 5, 2026). Originally assigned to: @thdxr 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 with exponential backoff delays. **User Experience**: The UI shows "Preparing write..." indefinitely, with the agent stuck in a loop. The only way to exit is to manually abort (Escape key). ## Detailed Timeline from Real Session Session `ses_3d5454748ffeA0QZlbOzaY4s4q` on project VibeBrowserProductPage: | Time | Event | Delay Since Last | |------|-------|------------------| | 02:50:05 | Stream started (claude-opus-4.5) | - | | 02:51:10 | **StreamIdleTimeoutError** (60s timeout) | 65s | | 02:51:12 | Retry #1 started | 2s backoff | | 02:52:16 | **StreamIdleTimeoutError** | 64s | | 02:52:20 | Retry #2 started | 4s backoff | | 02:53:23 | **StreamIdleTimeoutError** | 63s | | 02:53:31 | Retry #3 started | 8s backoff | | 02:54:34 | **StreamIdleTimeoutError** | 63s | | 02:54:50 | Retry #4 started | 16s backoff | | 02:55:53 | **StreamIdleTimeoutError** | 63s | | 02:56:23 | Retry #5 started | 30s backoff | | 02:57:27 | **StreamIdleTimeoutError** | 64s | | 02:57:52 | **User manually aborted** | - | Total time stuck: **~8 minutes** before user intervention. ## Raw Log Evidence ### StreamIdleTimeoutError Sequence ``` ERROR 2026-02-05T02:51:10 +60107ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ERROR 2026-02-05T02:52:16 +60096ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ERROR 2026-02-05T02:53:23 +60218ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ERROR 2026-02-05T02:54:34 +60211ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ERROR 2026-02-05T02:55:53 +60214ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ERROR 2026-02-05T02:57:27 +60133ms service=session.processor error=Stream idle timeout: no data received for 60000ms stack="StreamIdleTimeoutError: Stream idle timeout: no data received for 60000ms\n at <anonymous> (src/session/processor.ts:44:20)" process ``` ### Retry Pattern with Exponential Backoff ``` INFO 2026-02-05T02:50:05 service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream INFO 2026-02-05T02:51:12 +2002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream INFO 2026-02-05T02:52:20 +4002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream INFO 2026-02-05T02:53:31 +8003ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream INFO 2026-02-05T02:54:50 +16003ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream INFO 2026-02-05T02:56:23 +30002ms service=llm modelID=claude-opus-4.5 sessionID=ses_3d5454748ffeA0QZlbOzaY4s4q stream ``` Note the delays: 2s → 4s → 8s → 16s → 30s (capped at RETRY_MAX_DELAY_NO_HEADERS) ### Task Verification Shows 10 Empty Write Attempts From the reflection/task verification system: ``` ## Tools Used write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} write: {} ## Agent's Response You're right, I was overthinking. Let me just write the full page: ``` The Write tool was called **10 times with empty input `{}`** because the stream died during `tool-input-start` phase before the JSON input was fully parsed. ## Root Cause Analysis ### The Retry Loop Flow ``` User asks: "continue working on full product page" ↓ Model starts generating Write tool call with large content ↓ Provider API stalls (rate limit, internal processing, or output token exhaustion) ↓ 60 seconds pass with no stream data chunks ↓ StreamIdleTimeoutError thrown (processor.ts:44) ↓ Error converted to APIError with isRetryable: true (message-v2.ts:715) ↓ retry.ts.retryable() returns message string (line 62-64) ↓ processor.ts catches error, increments attempt, waits with backoff (line 403-420) ↓ New LLM.stream() call starts from scratch ↓ Model sees previous failed attempt with "Tool execution aborted" error ↓ Model tries THE SAME approach again ↓ REPEAT FOREVER (or until user aborts) ``` ### Why Doom Loop Detection Doesn't Trigger The existing doom loop detection in `processor.ts:207-232` checks: ```typescript if (part.state.status === "running" && part.state.input) { // Track same tool + same input called 3 times } ``` **This fails because:** 1. Stream dies during `tool-input-start` phase (before `tool-call` event) 2. Tool never reaches "running" status - it stays in "pending" 3. Input is always `{}` (empty) - JSON was never fully received 4. Cleanup marks tool as "error" with empty input 5. Each retry has a **different** tool call ID 6. Empty inputs `{}` are not detected as "same input" ### Code Path Evidence **message-v2.ts:711-720** - StreamIdleTimeoutError marked as retryable: ```typescript case e instanceof StreamIdleTimeoutError: return new MessageV2.APIError( { message: e.message, isRetryable: true, // <-- This causes infinite retries metadata: { timeoutMs: String(e.timeoutMs), }, }, { cause: e }, ).toObject() ``` **processor.ts:403-420** - Retry logic with no max attempts: ```typescript } catch (e: any) { log.error("process", { error: e, stack: JSON.stringify(e.stack) }) const error = MessageV2.fromError(e, { providerID: input.model.providerID }) const retry = SessionRetry.retryable(error) if (retry !== undefined) { attempt++ const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) SessionStatus.set(input.sessionID, { type: "retry", attempt, message: retry, next: Date.now() + delay, }) await SessionRetry.sleep(delay, input.abort).catch(() => {}) continue // <-- No max retry check for StreamIdleTimeoutError } // ... } ``` **processor.ts:442-458** - Cleanup marks incomplete tools as aborted: ```typescript for (const part of p) { if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") { await Session.updatePart({ ...part, state: { ...part.state, status: "error", error: "Tool execution aborted", // <-- Generic message, no actionable guidance // ... }, }) } } ``` ## Environment - **Provider**: github-copilot - **Model**: claude-opus-4.5 - **Stream idle timeout**: 60000ms (default) - **Tool**: write - **User task**: "continue working on full product page, target financial sectors" ## Suggested Fixes ### Option 1: Add max retries for StreamIdleTimeoutError (Recommended) ```typescript // In processor.ts let idleTimeoutRetries = 0 const MAX_IDLE_TIMEOUT_RETRIES = 3 // In catch block, before retry logic: if (e instanceof StreamIdleTimeoutError) { idleTimeoutRetries++ if (idleTimeoutRetries >= MAX_IDLE_TIMEOUT_RETRIES) { input.assistantMessage.error = MessageV2.fromError( new Error(`Stream repeatedly timed out (${MAX_IDLE_TIMEOUT_RETRIES} attempts). The model may be trying to generate content that exceeds output limits. Try breaking the task into smaller pieces.`), { providerID: input.model.providerID } ) Bus.publish(Session.Event.Error, { sessionID: input.assistantMessage.sessionID, error: input.assistantMessage.error, }) break // Exit the retry loop } } ``` ### Option 2: Detect repeated incomplete tool calls Track tools that fail during input generation (empty inputs): ```typescript // In processor.ts const incompleteToolAttempts: Record<string, number> = {} // In cleanup section (line 442-458): for (const part of p) { if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") { // Track incomplete tool attempts const inputSize = JSON.stringify(part.state.input || {}).length if (inputSize <= 2) { // Empty object "{}" incompleteToolAttempts[part.tool] = (incompleteToolAttempts[part.tool] || 0) + 1 if (incompleteToolAttempts[part.tool] >= DOOM_LOOP_THRESHOLD) { blocked = true // Add guidance to error message } } // ... rest of cleanup } } ``` ### Option 3: Better error message with actionable guidance Instead of generic "Tool execution aborted": ```typescript error: `Tool execution aborted: stream timed out after ${timeoutMs/1000}s while generating tool input. This often happens when attempting to write very large content. Consider breaking the write operation into smaller chunks.` ``` ### Option 4: Make StreamIdleTimeoutError non-retryable (simplest) ```typescript // In message-v2.ts:711-720 case e instanceof StreamIdleTimeoutError: return new MessageV2.APIError( { message: e.message, isRetryable: false, // <-- Stop automatic retries metadata: { timeoutMs: String(e.timeoutMs), }, }, { cause: e }, ).toObject() ``` This surfaces the error to the user immediately, who can then choose to retry or modify their request. ## Related Files - `packages/opencode/src/session/processor.ts` - Stream processing, idle timeout, doom loop detection, cleanup - `packages/opencode/src/session/message-v2.ts` - StreamIdleTimeoutError class, error conversion, isRetryable flag - `packages/opencode/src/session/retry.ts` - Retry logic, backoff calculation - `packages/opencode/src/session/prompt.ts` - Main agentic loop ## Additional Context This issue can occur with any provider when: 1. The model tries to generate very large tool inputs (like writing full files) 2. The provider has internal rate limiting or processing delays 3. The model hits output token limits during tool input generation 4. Network issues cause intermittent stalls The exponential backoff makes this particularly frustrating - after a few retries, the user is waiting 30+ seconds between each failed attempt, with no indication that the same error will keep occurring.
yindo added the perf label 2026-02-16 18:10:18 -05:00
Author
Owner

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

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

  • #11112: "always stuck at 'Preparing write...'" - EXACT DUPLICATE with same UI freeze symptom
  • #11313: "Long-running bash commands with large outputs cause truncation and agent retry loops" - Similar exponential retry loop pattern
  • #8002: "OpenCode hangs/freezes mid-stream on Starlink satellite internet connection" - Same stream stall and timeout symptoms
  • #10802: "TUI: Parent session appears stuck 'loading' when subagent is blocked" - Related to hung tool execution
  • #6959: "lsp bug: edit tool stalls after first diff; assistant hangs" - Tool stalls and never completes
  • #9377: "Forbidden: upstream connect error or disconnect/reset before headers" - Infinite retry loop on connection failure
  • #3088: "Indefinitely retry to fetch" - Same exponential backoff timeout issue
  • #5100: "Infinite error message" - Gets stuck in retry loop
  • #11282: "Issue: Opencode Fails to Write Files When Switching from Plan to Build Mode" - Model unable to complete write tool
  • #10367: "DeepSeek model stuck when writing code files" - Write tool stalls at input generation
  • #5890: "Incomplete JSON when writing out files" - Write tool JSON parsing failures causing retries
  • #11079: "[BUG] Write tool continuously getting aborted" - Write tool abort/retry pattern
  • #4255: "OpenCode v1.0.25 Hangs Indefinitely with LM Studio + Qwen Models" - Session hangs on tool execution

Most critical duplicates: #11112 (exact same symptoms), #11313, #8002, and #11079.

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: - #11112: "always stuck at 'Preparing write...'" - EXACT DUPLICATE with same UI freeze symptom - #11313: "Long-running bash commands with large outputs cause truncation and agent retry loops" - Similar exponential retry loop pattern - #8002: "OpenCode hangs/freezes mid-stream on Starlink satellite internet connection" - Same stream stall and timeout symptoms - #10802: "TUI: Parent session appears stuck 'loading' when subagent is blocked" - Related to hung tool execution - #6959: "lsp bug: edit tool stalls after first diff; assistant hangs" - Tool stalls and never completes - #9377: "Forbidden: upstream connect error or disconnect/reset before headers" - Infinite retry loop on connection failure - #3088: "Indefinitely retry to fetch" - Same exponential backoff timeout issue - #5100: "Infinite error message" - Gets stuck in retry loop - #11282: "Issue: Opencode Fails to Write Files When Switching from Plan to Build Mode" - Model unable to complete write tool - #10367: "DeepSeek model stuck when writing code files" - Write tool stalls at input generation - #5890: "Incomplete JSON when writing out files" - Write tool JSON parsing failures causing retries - #11079: "[BUG] Write tool continuously getting aborted" - Write tool abort/retry pattern - #4255: "OpenCode v1.0.25 Hangs Indefinitely with LM Studio + Qwen Models" - Session hangs on tool execution Most critical duplicates: #11112 (exact same symptoms), #11313, #8002, and #11079. Feel free to ignore if none of these address your specific case.
Author
Owner

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

Response to Duplicate Analysis

After reviewing the suggested duplicates, here's my assessment:

Closely Related Issues (Same Root Cause)

#11112 - "always stuck at 'Preparing write...'" - SAME SYMPTOMS, SAME ROOT CAUSE

  • User reports exact same pattern: Preparing write... → Tool execution aborted → retry → repeat
  • This issue provides the root cause analysis and fix proposals that #11112 lacks
  • Recommend merging the detailed analysis from this issue into #11112, or keeping this as the canonical technical issue

#11079 - "[BUG] Write tool continuously getting aborted" - SAME SYMPTOMS

  • Same provider (GitHub Copilot) and similar model (Sonnet 4.5 vs Opus 4.5)
  • Same "continuously getting aborted" pattern
  • Missing root cause analysis

Related But Different Issues

#11313 - "Long-running bash commands with large outputs cause truncation and agent retry loops"

  • Different trigger: bash output truncation, not stream idle timeout
  • Different tool: bash, not write
  • Similar outcome: retry loop, but different mechanism

#8002 - "OpenCode hangs/freezes mid-stream on Starlink satellite internet"

  • Similar symptom: stream stalls
  • Different cause: network instability vs. provider-side stall
  • May benefit from same fix (max retries for idle timeout)

Recommendation

This issue (#12234) provides:

  1. Detailed timeline with exact timestamps from real session logs
  2. Root cause analysis tracing the code path through processor.ts, message-v2.ts, and retry.ts
  3. Explanation of why doom loop detection fails for this specific case
  4. Four concrete fix proposals with code examples

I suggest either:

  • Option A: Close #11112 and #11079 as duplicates of this issue (since this has the analysis)
  • Option B: Keep this issue open as the "technical root cause" issue and link it from #11112/#11079

The core bug is: StreamIdleTimeoutError is marked isRetryable: true with no max retry limit, and doom loop detection doesn't catch empty-input tool failures.

@dzianisv commented on GitHub (Feb 5, 2026): ## Response to Duplicate Analysis After reviewing the suggested duplicates, here's my assessment: ### Closely Related Issues (Same Root Cause) **#11112 - "always stuck at 'Preparing write...'"** - ✅ **SAME SYMPTOMS, SAME ROOT CAUSE** - User reports exact same pattern: `Preparing write... → Tool execution aborted → retry → repeat` - This issue provides the **root cause analysis and fix proposals** that #11112 lacks - Recommend merging the detailed analysis from this issue into #11112, or keeping this as the canonical technical issue **#11079 - "[BUG] Write tool continuously getting aborted"** - ✅ **SAME SYMPTOMS** - Same provider (GitHub Copilot) and similar model (Sonnet 4.5 vs Opus 4.5) - Same "continuously getting aborted" pattern - Missing root cause analysis ### Related But Different Issues **#11313 - "Long-running bash commands with large outputs cause truncation and agent retry loops"** - Different trigger: bash output truncation, not stream idle timeout - Different tool: bash, not write - Similar outcome: retry loop, but different mechanism **#8002 - "OpenCode hangs/freezes mid-stream on Starlink satellite internet"** - Similar symptom: stream stalls - Different cause: network instability vs. provider-side stall - May benefit from same fix (max retries for idle timeout) ### Recommendation This issue (#12234) provides: 1. **Detailed timeline with exact timestamps** from real session logs 2. **Root cause analysis** tracing the code path through `processor.ts`, `message-v2.ts`, and `retry.ts` 3. **Explanation of why doom loop detection fails** for this specific case 4. **Four concrete fix proposals** with code examples I suggest either: - **Option A**: Close #11112 and #11079 as duplicates of this issue (since this has the analysis) - **Option B**: Keep this issue open as the "technical root cause" issue and link it from #11112/#11079 The core bug is: **`StreamIdleTimeoutError` is marked `isRetryable: true` with no max retry limit, and doom loop detection doesn't catch empty-input tool failures.**
Author
Owner

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

Additional Root Cause Analysis: GitHub Copilot Does NOT Stream Tool Arguments

After debugging a live stuck session, I found the actual root cause of why the timeout fires:

The Problem: Provider Buffering

GitHub Copilot + Claude does NOT stream tool-input-delta events incrementally. Instead, it buffers the entire tool call JSON internally before sending it.

Timeline from Live Debug Session

Time Event Delta
17:40:09 LLM stream starts -
17:40:13 tool-input-start event (edit tool announced) +4s
17:40:13 Last message.part.updated -
17:41:13 StreamIdleTimeoutError fires +60s of complete silence
17:41:28 Edit permission evaluated, file.edited succeeds! +15s after timeout

Key Finding: The Edit SUCCEEDS After Timeout!

ERROR 2026-02-05T17:41:13 +60045ms service=session.processor error=Stream idle timeout: no data received for 60000ms
INFO  2026-02-05T17:41:28 +0ms service=permission permission=edit pattern=context.md action={"permission":"edit","pattern":"*","action":"allow"} evaluated
INFO  2026-02-05T17:41:28 +0ms service=bus type=file.edited publishing

The edit tool actually works - the file gets modified. But the timeout fires first because:

  1. Claude announces tool-input-start (edit tool) at T+4s
  2. Claude generates the large JSON for the edit tool internally (file content is large)
  3. Zero tool-input-delta events are sent during 60+ seconds of generation
  4. Timeout fires at T+64s
  5. Complete tool JSON finally arrives at T+79s
  6. Edit executes successfully
  7. But retry loop already started due to timeout

Evidence: No Delta Events

The opencode processor handles these events:

case "tool-input-delta":
  break  // These events would reset the timer if they arrived

case "tool-input-end":
  break

But GitHub Copilot never sends tool-input-delta events while generating tool arguments. It buffers everything and sends the complete tool-call event only after generation finishes.

This Explains Why:

  1. Only GitHub Copilot + Claude is affected - Other providers may stream tool deltas
  2. Large file writes fail more often - More content = longer generation = more likely to timeout
  3. The edits actually succeed - But the retry loop starts anyway
  4. Each retry also succeeds but also times out - Creating the infinite loop

Proposed Fix

Option A: Don't timeout during pending tool input

Track when tool-input-start is received but tool-call hasn't arrived yet. Disable/extend the idle timeout during this window.

// In processor.ts
let pendingToolInputs = new Set<string>()

case "tool-input-start":
  pendingToolInputs.add(value.id)
  // ... existing code

case "tool-call":
  pendingToolInputs.delete(value.toolCallId)
  // ... existing code

// In withIdleTimeout or the main loop:
// If pendingToolInputs.size > 0, use extended timeout (e.g., 5 minutes)

Option B: Provider-specific timeouts

Allow configuring longer timeouts for providers known to buffer tool calls:

# opencode.yaml
providers:
  github-copilot:
    stream_idle_timeout: 300000  # 5 minutes for tool generation

Option C: Report to GitHub

This is arguably a bug/limitation in GitHub Copilot's Claude integration. They should be streaming tool-input-delta events as the model generates tool arguments, not buffering the entire response.

Related Log Evidence

The session ses_3d9bcd25affeBTYwpk5X8qCz7k shows this pattern repeating:

  • Stream starts
  • 4 seconds of activity (text + tool-input-start)
  • 60 seconds of complete silence (no tool-input-delta)
  • Timeout fires
  • 15-25 seconds later, edit completes
  • Retry starts
  • Repeat infinitely
@dzianisv commented on GitHub (Feb 5, 2026): ## Additional Root Cause Analysis: GitHub Copilot Does NOT Stream Tool Arguments After debugging a live stuck session, I found the **actual root cause** of why the timeout fires: ### The Problem: Provider Buffering **GitHub Copilot + Claude does NOT stream `tool-input-delta` events incrementally.** Instead, it buffers the entire tool call JSON internally before sending it. ### Timeline from Live Debug Session | Time | Event | Delta | |------|-------|-------| | `17:40:09` | LLM stream starts | - | | `17:40:13` | `tool-input-start` event (edit tool announced) | +4s | | `17:40:13` | Last `message.part.updated` | - | | `17:41:13` | **StreamIdleTimeoutError fires** | **+60s of complete silence** | | `17:41:28` | Edit permission evaluated, **file.edited succeeds!** | +15s after timeout | ### Key Finding: The Edit SUCCEEDS After Timeout! ``` ERROR 2026-02-05T17:41:13 +60045ms service=session.processor error=Stream idle timeout: no data received for 60000ms INFO 2026-02-05T17:41:28 +0ms service=permission permission=edit pattern=context.md action={"permission":"edit","pattern":"*","action":"allow"} evaluated INFO 2026-02-05T17:41:28 +0ms service=bus type=file.edited publishing ``` The edit tool **actually works** - the file gets modified. But the timeout fires first because: 1. Claude announces `tool-input-start` (edit tool) at T+4s 2. Claude generates the large JSON for the edit tool **internally** (file content is large) 3. **Zero `tool-input-delta` events are sent** during 60+ seconds of generation 4. Timeout fires at T+64s 5. Complete tool JSON finally arrives at T+79s 6. Edit executes successfully 7. But retry loop already started due to timeout ### Evidence: No Delta Events The opencode processor handles these events: ```typescript case "tool-input-delta": break // These events would reset the timer if they arrived case "tool-input-end": break ``` But **GitHub Copilot never sends `tool-input-delta` events** while generating tool arguments. It buffers everything and sends the complete `tool-call` event only after generation finishes. ### This Explains Why: 1. **Only GitHub Copilot + Claude is affected** - Other providers may stream tool deltas 2. **Large file writes fail more often** - More content = longer generation = more likely to timeout 3. **The edits actually succeed** - But the retry loop starts anyway 4. **Each retry also succeeds but also times out** - Creating the infinite loop ### Proposed Fix **Option A: Don't timeout during pending tool input** Track when `tool-input-start` is received but `tool-call` hasn't arrived yet. Disable/extend the idle timeout during this window. ```typescript // In processor.ts let pendingToolInputs = new Set<string>() case "tool-input-start": pendingToolInputs.add(value.id) // ... existing code case "tool-call": pendingToolInputs.delete(value.toolCallId) // ... existing code // In withIdleTimeout or the main loop: // If pendingToolInputs.size > 0, use extended timeout (e.g., 5 minutes) ``` **Option B: Provider-specific timeouts** Allow configuring longer timeouts for providers known to buffer tool calls: ```yaml # opencode.yaml providers: github-copilot: stream_idle_timeout: 300000 # 5 minutes for tool generation ``` **Option C: Report to GitHub** This is arguably a bug/limitation in GitHub Copilot's Claude integration. They should be streaming `tool-input-delta` events as the model generates tool arguments, not buffering the entire response. ### Related Log Evidence The session `ses_3d9bcd25affeBTYwpk5X8qCz7k` shows this pattern repeating: - Stream starts - 4 seconds of activity (text + tool-input-start) - **60 seconds of complete silence** (no tool-input-delta) - Timeout fires - 15-25 seconds later, edit completes - Retry starts - Repeat infinitely
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#8575