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
TUI client fails with socket closed error on stale keep-alive connections #54160
The TUI HTTP client fails with "The socket connection was closed unexpectedly" when reusing stale HTTP keep-alive connections. Users must press Enter multiple times to retry manually.
To Reproduce
Start opencode TUI
Send a message
Wait for idle period (duration varies by network environment)
Send another message
Toast shows: "Transport: The socket connection was closed unexpectedly"
Thanks for the detailed report. I could reproduce this on v2.0.26, and it still happens on the latest v2 branch (5cfe3b4).
What I did, using a proxy between the TUI and the server:
Sent a first message and waited for the reply.
Made the idle keep-alive connections stop forwarding without notifying the client, which is what an aggressive proxy or endpoint agent does.
Submitted a second message, then had the proxy reset the stale connection.
The TUI showed "Failed to send prompt — Failed to switch model: Transport: The socket connection was closed unexpectedly" and left the prompt unsent. Pressing Enter again sent it on a fresh connection, as you described. The recording and screenshot below show this, and the script below reproduces it.
A side observation: Bun's fetch already retried GET requests on a stale connection by itself, but not POSTs. That is why sending a message (the model switch and the prompt itself) is where this shows up. Your PR #54154 is noted alongside this issue for the maintainers.
// Reproduces #54160: after an idle keep-alive connection between the TUI and the// server is dropped by the network (proxy / firewall / security agent), the next// prompt fails with "Transport: The socket connection was closed unexpectedly"// instead of being retried on a fresh connection.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),network: true,// route the TUI <-> server traffic through a controllable proxytui: {recording: true},},({ ui, llm, network })=>Effect.gen(function*(){yield*llm.serve((_req,i)=>Stream.make(Llm.text(`Reply number ${i+1}.`)))yield*ui.submit("first message")yield*ui.waitFor("Reply number 1.")yield*Effect.sleep(2000)// Idle period: a middlebox (corporate proxy / EDR agent) has silently stopped// forwarding the idle keep-alive connections; the client still believes they are open.yield*network.set({blackhole: true})yield*ui.submit("second message")yield*Effect.sleep(1500)// The middlebox answers the request written to the stale socket by resetting it.constkilled=yield*network.killConnections()yield*network.clear()yield*Effect.log(`reset ${killed} stale proxied connections`)yield*Effect.sleep(300)yield*ui.screenshot("right-after-reset")// Either the second reply arrives, or the transport error toast appears.constoutcome=yield*Effect.raceFirst(ui.waitFor("Reply number 2.",{timeout: 20000}).pipe(Effect.as("NOT REPRODUCED: second reply arrived")),ui.waitFor("Transport",{timeout: 20000}).pipe(Effect.as("REPRODUCED: transport error shown")),).pipe(Effect.catch(()=>Effect.succeed("neither reply nor error within 20s")))yield*ui.screenshot("after-second-message")yield*Effect.log(outcome)// The prompt is still in the input; a manual retry (Enter) goes through on a fresh connection.yield*Effect.sleep(2000)yield*ui.enter()yield*ui.waitFor("Reply number 2.",{timeout: 15000}).pipe(Effect.tap(()=>Effect.log("manual retry with Enter succeeded")),Effect.catch(()=>Effect.log("manual retry did not show the reply")),)yield*Effect.sleep(3000)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
opencode-agent commentedon Oct 9, 2026
Thanks for the detailed report. I could reproduce this on v2.0.26, and it still happens on the latest
v2branch (5cfe3b4).What I did, using a proxy between the TUI and the server:
The TUI showed "Failed to send prompt — Failed to switch model: Transport: The socket connection was closed unexpectedly" and left the prompt unsent. Pressing Enter again sent it on a fresh connection, as you described. The recording and screenshot below show this, and the script below reproduces it.
A side observation: Bun's fetch already retried GET requests on a stale connection by itself, but not POSTs. That is why sending a message (the model switch and the prompt itself) is where this shows up. Your PR #54154 is noted alongside this issue for the maintainers.
Related reports:
verbose: truein the second argument to fetch() #53829: the same ECONNRESET message in the desktop apprecording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.