You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
v2: WebSocket idle-timeout bypasses the 3-timeout retry cap, so a dead stream can hold a step for hours #57127
A WebSocket stream that goes silent fails with transport code idle-timeout after 30 minutes (packages/core/src/session/model-transport.ts:418-424, IDLE_TIMEOUT = "30 minutes"). The retry policy only counts code === "Timeout" toward MAX_TIMEOUT_RETRIES = 3 (packages/core/src/model-retry.ts:53). So an idle-timeout falls under the general 10-retry allowance, and one dead step can hold a session or subagent for hours.
This shows up after a laptop sleeps or the network drops with agents mid-run. On a v2.0.32 install over one week, 264 model requests failed on transport and took 54.8 request-hours. One review subagent spent 243 of its 256 minutes in 9 failed requests ("Timed out waiting for WebSocket data", "WebSocket closed with code 1006"), each about 15-40 min. The agent also sees these as failed tool calls with empty input (patch {}), so it changes its own behavior instead of recognising a provider stall.
Expected: an idle-timeout counts toward the timeout cap like an HTTP Timeout, the same reasoning as the comment above MAX_TIMEOUT_RETRIES.
Possible fix: in model-retry.ts, count reason.code === "idle-timeout" (WebSocket) along with "Timeout", plus a policy test next to the existing HTTP timeout test.
Plugins
Local plugins only (cache TTL, cost); none touch transport or retry.
Thanks for the detailed report and the pointers into the code. I can confirm this.
I tested the session retry policy directly. I fed it repeated failures with the same idle-timeout error that model-transport.ts raises when the WebSocket stream goes silent, and compared that with an HTTP Timeout:
HTTP Timeout: retried 3 times, then stopped (MAX_TIMEOUT_RETRIES).
WebSocket idle-timeout: retried 10 times under the general allowance, then stopped.
Since each idle-timeout attempt waits 30 minutes before failing, one step can be held for hours. That goes against the reason the comment above the cap gives for having it. The results were the same on v2.0.32 and on the latest dev (ce15cd8). The test script below reproduces it.
// Copy into packages/core/test/ of an opencode checkout and run: bun test test/idle-timeout-retry.test.ts (from packages/core)import{describe,expect}from"bun:test"import{AIError,TransportError}from"@opencode/ai"import{Agent}from"@opencode/schema/agent"import{Model}from"@opencode/schema/model"import{Provider}from"@opencode/core/provider"import{SessionSchema}from"@opencode/core/session/schema"import{SessionRunnerRetry}from"@opencode/core/session/runner/retry"import{toSessionError}from"@opencode/core/session/to-session-error"import{Effect}from"effect"import{it}from"./lib/effect"consthttpTimeout=newAIError({reason: newTransportError({message: "Timed out",transport: "http",operation: "request",code: "Timeout"}),})// Same shape model-transport.ts produces after the 30-minute WebSocket IDLE_TIMEOUT.constwsIdle=newAIError({reason: newTransportError({message: "Timed out waiting for WebSocket data",transport: "websocket",operation: "read",code: "idle-timeout",phase: "receive",delivery: "accepted",}asany),})constinput=(cause: AIError)=>({
cause,error: toSessionError(cause),agent: Agent.ID.make("build"),model: Model.Ref.make({id: Model.ID.make("model"),providerID: Provider.ID.make("provider")}),hook: ()=>Effect.void,retry: SessionRunnerRetry.isRetryable(cause),})constdecisions=(cause: AIError,count: number)=>Effect.gen(function*(){constpolicy=yield*SessionRunnerRetry.policy(SessionSchema.ID.make("ses_retry"))constresults: boolean[]=[]for(leti=0;i<count;i++)results.push((yield*policy(input(cause))).retry)returnresults})describe("timeout cap",()=>{it.effect("http Timeout is capped at 3 retries",()=>Effect.gen(function*(){constr=yield*decisions(httpTimeout,11)console.log("http Timeout :",JSON.stringify(r))expect(r.slice(0,4)).toEqual([true,true,true,false])}),)it.effect("websocket idle-timeout should be capped at 3 retries too",()=>Effect.gen(function*(){constr=yield*decisions(wsIdle,11)console.log("ws idle-timeout:",JSON.stringify(r),"retryable:",SessionRunnerRetry.isRetryable(wsIdle))expect(r.slice(0,4)).toEqual([true,true,true,false])}),)})
I reproduced this on dev 367c8fb and pushed a scoped fix: two-file diff, commit a2162bf.
idle-timeout now shares the existing three-timeout counter with HTTP Timeout. Regressions cover repeated idle timeouts, mixed transport timeouts and the unchanged connection-failure allowance. The two regressions fail before the fix; 50 repeated policy tests and 46 related tests pass afterward. Independent review found no actionable issues, and lint/all 34 typecheck tasks pass. The Windows full-suite comparison has the same 151 failures on both versions, plus the two expected regression failures before the fix.
GitHub currently blocks my new PR with this repository's three-open-PR limit. The tested branch is available for review; no PR has been created, and I will retain my existing PRs.
Activity
opencode-agent commentedon Oct 19, 2026
Thanks for the detailed report and the pointers into the code. I can confirm this.
I tested the session retry policy directly. I fed it repeated failures with the same
idle-timeouterror thatmodel-transport.tsraises when the WebSocket stream goes silent, and compared that with an HTTPTimeout:Timeout: retried 3 times, then stopped (MAX_TIMEOUT_RETRIES).idle-timeout: retried 10 times under the general allowance, then stopped.Since each idle-timeout attempt waits 30 minutes before failing, one step can be held for hours. That goes against the reason the comment above the cap gives for having it. The results were the same on v2.0.32 and on the latest
dev(ce15cd8). The test script below reproduces it.Related:
Reproduction script (idle-timeout-retry.test.ts)
Run it:
bun idle-timeout-retry.test.tsHootRock commentedon Oct 19, 2026
I reproduced this on dev
367c8fband pushed a scoped fix: two-file diff, commita2162bf.idle-timeoutnow shares the existing three-timeout counter with HTTPTimeout. Regressions cover repeated idle timeouts, mixed transport timeouts and the unchanged connection-failure allowance. The two regressions fail before the fix; 50 repeated policy tests and 46 related tests pass afterward. Independent review found no actionable issues, and lint/all 34 typecheck tasks pass. The Windows full-suite comparison has the same 151 failures on both versions, plus the two expected regression failures before the fix.GitHub currently blocks my new PR with this repository's three-open-PR limit. The tested branch is available for review; no PR has been created, and I will retain my existing PRs.
AI-assisted; reviewed and tested locally.