Description
When the web UI is served by opencode serve, its event stream client gives each /api/event attempt a hard 2s deadline to receive and parse the first server.connected event. After a failure it retries on a fixed 1s delay with no backoff and no attempt limit. Behind a network path that delays the start of streamed responses (likely a corporate proxy or TLS-inspecting AV), the connection works but is slow to start, and the UI stays in its loading state for minutes: the session list shows "loading" and no agents can be selected.
Source: packages/client/src/solid/connection.ts (same code in v2.0.23, v2.0.24 and the current v2 head):
const connectTimeout = 2_000
const reconnectDelay = 1_000
// connect(): the deadline is cleared only after the first parsed event
const timeout = setTimeout(() => request.abort(new Error("Timed out connecting to server")), connectTimeout)
const first = await iterator.next()
if (first.value.type !== "server.connected") return { error: new Error("Event stream did not start with server.connected"), ... }
clearTimeout(timeout)
// runStream(): after every failure
await wait(reconnectDelay, controller.signal) // fixed 1s
The web client passes no reconnect resolver, so this fixed loop is the only recovery path. The onActivity/touch() hook already sees incoming bytes, including keepalive comments, but it only feeds the idle watchdog after the connection is established. It does not count toward the connect deadline.
Observed
- The server sits behind a reverse proxy and sends
server.connected immediately. We verified this with curl, and the response headers are content-type: text/event-stream, cache-control: no-cache, no-transform and x-accel-buffering: no. A : heartbeat comment follows every 15s.
- For one user on a slow, buffering network path, the server log showed a
/api/event request every ~3s, each lasting almost exactly 2.0s before being interrupted (InterruptError, status 200). There were about 140 such attempts over about 7 minutes before one succeeded and the UI loaded.
- Another user on the same host produced more than 3000 of these 2s aborts over several days.
The 2s deadline turns a slow but working network into minutes of loading, and the fixed 1s retry keeps hitting the server the whole time.
Proposed fix
- Make the connect deadline more tolerant (e.g. 10–15s) and/or configurable, or count any received bytes (including SSE comments, via the existing
onActivity hook) as progress instead of requiring the parsed server.connected event within 2s.
- Use exponential backoff with jitter for reconnects (e.g. 1s → 2s → 4s …, capped at ~30s), and reset it after a successful connection.
- After N failed attempts, show a visible "cannot establish live connection" state instead of loading indefinitely.
- Optional, server side: send an initial padding comment so that proxies which buffer by response size flush the first event.
Related
Plugins
none
OpenCode version
2.0.23 (code unchanged in 2.0.24 and on v2)
Steps to reproduce
- Run
opencode serve and open the web UI through a network path that holds back the start of streamed responses for more than 2s (e.g. a TLS-inspecting proxy/AV, or simulate with a proxy that holds the first bytes of text/event-stream responses for 3s).
- The UI stays on loading. The server log shows
/api/event requests every ~3s, each aborted after ~2.0s.
Operating System
Server: Linux. Client: browser behind a corporate network.
Terminal
N/A (web UI)
Description
When the web UI is served by
opencode serve, its event stream client gives each/api/eventattempt a hard 2s deadline to receive and parse the firstserver.connectedevent. After a failure it retries on a fixed 1s delay with no backoff and no attempt limit. Behind a network path that delays the start of streamed responses (likely a corporate proxy or TLS-inspecting AV), the connection works but is slow to start, and the UI stays in its loading state for minutes: the session list shows "loading" and no agents can be selected.Source:
packages/client/src/solid/connection.ts(same code in v2.0.23, v2.0.24 and the currentv2head):The web client passes no
reconnectresolver, so this fixed loop is the only recovery path. TheonActivity/touch()hook already sees incoming bytes, including keepalive comments, but it only feeds the idle watchdog after the connection is established. It does not count toward the connect deadline.Observed
server.connectedimmediately. We verified this with curl, and the response headers arecontent-type: text/event-stream,cache-control: no-cache, no-transformandx-accel-buffering: no. A: heartbeatcomment follows every 15s./api/eventrequest every ~3s, each lasting almost exactly 2.0s before being interrupted (InterruptError, status 200). There were about 140 such attempts over about 7 minutes before one succeeded and the UI loaded.The 2s deadline turns a slow but working network into minutes of loading, and the fixed 1s retry keeps hitting the server the whole time.
Proposed fix
onActivityhook) as progress instead of requiring the parsedserver.connectedevent within 2s.Related
Accept: text/event-streamheader. That may help for some AV setups, but the deadline and retry behaviour stay the same for other slow paths.packages/app/src/context/server-sdk.tsx, not increateClientConnection.connectTimeoutto 10s as part of an unrelated, broader PR.Plugins
none
OpenCode version
2.0.23 (code unchanged in 2.0.24 and on
v2)Steps to reproduce
opencode serveand open the web UI through a network path that holds back the start of streamed responses for more than 2s (e.g. a TLS-inspecting proxy/AV, or simulate with a proxy that holds the first bytes oftext/event-streamresponses for 3s)./api/eventrequests every ~3s, each aborted after ~2.0s.Operating System
Server: Linux. Client: browser behind a corporate network.
Terminal
N/A (web UI)