[PR #13846] fix: add timeout and retry recovery for subagent LLM streams #14839

Open
opened 2026-02-16 18:19:35 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/13846

State: open
Merged: No


Summary

Fixes #13841 — Explore subagent hangs indefinitely with Anthropic Claude Opus 4.6 (and other providers) due to four compounding gaps in timeout/retry handling.

Changes

1. Default 300s fetch timeout (provider.ts)

The config schema documented a 300s default, but the field was .optional() with no enforced default. When unconfigured, options["timeout"] was undefined and the AbortSignal.timeout() guard was skipped entirely. Combined with Bun's socket timeout being explicitly disabled (timeout: false), there was no fallback timeout of any kind.

Now: const timeout = options["timeout"] ?? 300_000 — always applies the documented default unless explicitly set to false.

2. Stream-idle watchdog (processor.ts)

A connection that stays open but stops delivering SSE chunks was never detected. The LLM.stream() call only passed the session's AbortSignal with no idle detection.

Now: withIdleTimeout() async generator wraps stream.fullStream with a 2-minute idle timer that resets on each received chunk. On timeout, throws a StreamIdleTimeoutError (marked retryable via message-v2.ts).

3. Session-level retry cap (processor.ts)

The retry loop had no maximum attempt count — attempt++ with no ceiling. If errors stayed retryable (e.g. rate limits, overloaded), it looped forever.

Now: MAX_RETRY_ATTEMPTS = 10 — after exceeding the cap, the error is surfaced to the user instead of retrying.

4. Subagent timeout (task.ts)

The Task tool awaited SessionPrompt.prompt() with no timeout wrapper. Cancellation only happened when the parent session was manually aborted.

Now: wraps the prompt call with abortAfterAny(10 * 60 * 1000, ctx.abort) — 10-minute hard ceiling that also cascades to SessionPrompt.cancel().

Testing

  • All 980 existing tests pass (0 failures)
  • Changes are minimal and surgical — 59 lines added, 6 removed across 4 files

Related Issues

  • #11865 — Same symptom with Codex/OpenAI
  • #13395 — Subagent API errors silently swallowed
  • #9003 — Main agent hangs because of subagent (explore)
  • #12234 — Infinite retry loop on StreamIdleTimeoutError
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/13846 **State:** open **Merged:** No --- ## Summary Fixes #13841 — Explore subagent hangs indefinitely with Anthropic Claude Opus 4.6 (and other providers) due to four compounding gaps in timeout/retry handling. ## Changes ### 1. Default 300s fetch timeout (`provider.ts`) The config schema documented a 300s default, but the field was `.optional()` with no enforced default. When unconfigured, `options["timeout"]` was `undefined` and the `AbortSignal.timeout()` guard was skipped entirely. Combined with Bun's socket timeout being explicitly disabled (`timeout: false`), there was **no fallback timeout of any kind**. Now: `const timeout = options["timeout"] ?? 300_000` — always applies the documented default unless explicitly set to `false`. ### 2. Stream-idle watchdog (`processor.ts`) A connection that stays open but stops delivering SSE chunks was never detected. The `LLM.stream()` call only passed the session's `AbortSignal` with no idle detection. Now: `withIdleTimeout()` async generator wraps `stream.fullStream` with a 2-minute idle timer that resets on each received chunk. On timeout, throws a `StreamIdleTimeoutError` (marked retryable via `message-v2.ts`). ### 3. Session-level retry cap (`processor.ts`) The retry loop had no maximum attempt count — `attempt++` with no ceiling. If errors stayed retryable (e.g. rate limits, overloaded), it looped forever. Now: `MAX_RETRY_ATTEMPTS = 10` — after exceeding the cap, the error is surfaced to the user instead of retrying. ### 4. Subagent timeout (`task.ts`) The Task tool awaited `SessionPrompt.prompt()` with no timeout wrapper. Cancellation only happened when the parent session was manually aborted. Now: wraps the prompt call with `abortAfterAny(10 * 60 * 1000, ctx.abort)` — 10-minute hard ceiling that also cascades to `SessionPrompt.cancel()`. ## Testing - All 980 existing tests pass (0 failures) - Changes are minimal and surgical — 59 lines added, 6 removed across 4 files ## Related Issues - #11865 — Same symptom with Codex/OpenAI - #13395 — Subagent API errors silently swallowed - #9003 — Main agent hangs because of subagent (explore) - #12234 — Infinite retry loop on StreamIdleTimeoutError
yindo added the pull-request label 2026-02-16 18:19:35 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14839