Skip to content

tui: startup reconnect loop on Windows - 2s SSE first-frame timeout aborts /api/event (shared and standalone) #55741

Closed

Description

@AcSsalazar

Summary

On Windows, the V2 TUI repeatedly fails to establish the event stream (GET /api/event, SSE) at startup. Each attempt is aborted by the client after ~2.0 s with Timed out connecting to server, then retried. The TUI shows "Restarting service... Your session will resume automatically." (or "Connection lost... Reconnecting to the server automatically.") for a long time — observed up to 80 attempts (~4 min) — even though the session keeps working behind the overlay. It reproduces with the shared background service AND with a private/standalone server.

Environment

  • opencode version: 2.0.29
  • OS: Windows 10.0.26300.9711 (win32 x64)
  • Terminal: Windows Terminal (tab running Git Bash); also reproduced launching from cmd/PowerShell
  • Shell: Git Bash (SHELL=/usr/bin/bash); ComSpec=C:\WINDOWS\system32\cmd.exe
  • Install/channel: npm global install, @opencode/[email protected] under %APPDATA%\npm; channel latest
  • Active plugins: none found in user or project config (~/.config/opencode has only cli.json + service.json; project has no opencode.json/.opencode). Logs still report plugins=12 during reconciliation.

Reproduction

  1. On Windows, install opencode 2.0.29 via npm global (npm i -g @opencode/[email protected]).
  2. Open opencode in a terminal (Git Bash, cmd, or PowerShell).
  3. Observe the "Restarting service..." overlay and repeated reconnects.
  4. Inspect ~/.local/share/opencode/log/opencode.log.

Expected Behavior

The event stream should connect promptly (first server.connected frame within the client's first-frame timeout) and the TUI should become interactive without a long reconnect overlay.

Actual Behavior

Every GET /api/event is aborted by the client after ~2.0 s. The server only logs the response when the client disconnects (http.span≈2000). The client retries many times before connecting.

Log excerpt (timestamps UTC):

... event stream connecting component=client attempt=0 role=cli
... event stream disconnected component=client attempt=1 error="Timed out connecting to server" role=cli
... event stream connecting component=client attempt=1 role=cli
... event stream disconnected component=client attempt=2 error="Timed out connecting to server" role=cli
...
... event stream connecting component=client attempt=59 role=cli
... event stream disconnected component=client attempt=60 error="Timed out connecting to server" role=cli
... event stream connecting component=client attempt=60 role=cli
... event stream connected component=client role=cli

Each attempt lasts ~2000 ms, for tens of attempts in a row.

Additional Context

  • Reproduces with the shared background service (serve --service) AND with a private server (serve --stdio --port 0), so this is not specific to the managed service.
  • The ~2 s first-frame timeout in the TUI SDK (packages/tui/src/context/sdk.tsx) is also referenced by 2.0: managed-service restart causes reconnect herd and resource spikes #36285 and cli: managed reconnect waits until fourth failure to ensure service #36581.
  • Workarounds tried without success: reinstalling, installing an older version, and disabling the shared service (service set disabled true). Disabling the service made no difference because the private server shows the same timeout.
  • After the shared service is stopped (opencode service stop) while a TUI is connected, the client enters a reconnect storm ("Connection lost / Reconnecting...").
  • Server is the Bun-based V2 server on Windows; it subscribes watchers to drive roots (C:\, E:\).

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and logs. Several things can cause the same 2 s /api/event timeout loop on Windows, and they look the same in the log. To work out which one you're hitting, could you share:

    1. Proxy variables: the output of env | grep -i proxy (Git Bash) or set | findstr /i proxy (cmd). If HTTP_PROXY/HTTPS_PROXY are set, does it still happen with NO_PROXY=127.0.0.1,localhost?
    2. Security software: are you running antivirus or endpoint protection other than plain Windows Defender (e.g. Kaspersky, ESET, a corporate TLS-inspecting agent)? If you can, does it still happen with it briefly disabled?
    3. How fast the server responds: while the overlay is showing, find the server's port in the log and run curl -N http://127.0.0.1:<port>/api/event (add the same auth header or password the CLI uses, if any). Does the server.connected event show up immediately, or only after a few seconds?
    4. Directory: which directory you start opencode from. The log shows watchers on C:\ and E:\, so are you launching it from a drive root? Does it also happen when you start it inside a normal project folder?

    You can reply here or edit the issue. If you edit it, please also leave a comment saying you've updated it, so we know when you're done.

    Here are possibly related issues, ordered from most to least likely. Please check whether one of them already covers your problem:

  2. AcSsalazar commented on Oct 15, 2026

    @AcSsalazar
    Author

    Root cause narrowed down — this is server-side, and reproducible with an external HTTP client (not the TUI).

    I measured /api/event directly with curl against the running server (auth: HTTP Basic, opencode:<password-from-service.json>):

    • GET /api/info → first byte in ~8 ms.
    • GET /api/event → first byte at exactly ~15.003 s, reproducible (15.003816 s / 15.003771 s). First bytes received:
    data: {"id":"...","type":"server.connected","data":{}}
    
    : heartbeat
    
    : heartbeat
    

    So the SSE response does not flush its headers or the initial server.connected frame immediately; the first flush happens at the heartbeat tick. The TUI aborts the stream at ~2 s (Timed out connecting to server), so it only ever connects when a bus event happens to be published inside that 2 s window — which is exactly the reconnect loop / "Restarting service..." overlay reported here.

    This matches packages/server/src/handlers/event.ts (V2):

    const connected = { id: Event.ID.create(), type: "server.connected", data: {} }
    const output = Stream.unwrap(
      feed.subscribe.pipe(Effect.map((live) =>
        Stream.make(EventFeed.frame(connected)).pipe(Stream.concat(live)))),
    )
    const heartbeat = Stream.tick("15 seconds").pipe(Stream.map(() => ": heartbeat\n\n"))
    return HttpServerResponse.stream(
      output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }), Stream.encodeText),
      { contentType: "text/event-stream",
        headers: { "Cache-Control": "no-cache, no-transform",
                   "X-Accel-Buffering": "no",
                   "X-Content-Type-Options": "nosniff" } },
    )

    The initial server.connected frame is produced only after the live feed is acquired (EventV2.allBounded), and the streaming response does not send headers until its first element is pulled — which here is the 15 s heartbeat.

    Environment

    • Windows 10.0.26300.9711 (win32 x64)
    • npm global install, channel latest
    • Reproduced on 2.0.28 and 2.0.29, with the shared service and a private/standalone server
    • No HTTP/HTTPS proxy (WinHTTP direct, WinINET ProxyEnable=0, no *_PROXY env vars)
    • All other endpoints (/api/info, /api/provider, /api/model, /api/config, ...) respond in <30 ms

    Suggested fix

    Flush the SSE response headers immediately and emit server.connected before/independently of acquiring the live feed, instead of relying on the 15 s heartbeat (or the next bus event) for the first flush. Alternatively/additionally, raise the TUI's ~2 s first-frame timeout so it tolerates a slow first flush.

  3. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the curl measurements, they help a lot.

    I tried this on Linux with 2.0.29 (opencode serve, started both in a project and from /) and couldn't reproduce it. /api/event sent its headers, the server.connected frame and a first : heartbeat within 1–4 ms, about as fast as /api/info. The 15 s heartbeat timer also fires once as soon as the stream opens. In your capture, server.connected arrived together with two heartbeats at ~15 s. So the server seems to have written the first frame right away, and something on your machine held those bytes until the next heartbeat about 15 s later. That points to something specific to Windows rather than the event handler waiting before it starts. It could be how the Bun-based server streams on Windows, or software that inspects local HTTP traffic.

    This needs a maintainer with a Windows machine to dig further. In the meantime, these would help narrow it down:

  4. AcSsalazar commented on Oct 15, 2026

    @AcSsalazar
    Author

    Also reproduced on the dev channel build @opencode/[email protected]: isolated opencode serve on Windows, GET /api/event first byte at 15.008 s (same as 2.0.28/2.0.29). So this is not fixed in current builds, stable or dev.

    Additional evidence: if any bus event is published shortly after subscribing, the stream flushes immediately. Subscribing then calling POST /api/location/reload ~1.5 s later yields the first byte at 1.53 s (server.connected + a burst of *.updated events) instead of 15 s. This confirms the initial frame/headers are simply not flushed until a second chunk arrives (heartbeat or any bus event).

  5. moaberdan commented on Oct 15, 2026

    @moaberdan

    Reproduced on my machine as well.

    Environment

    • opencode version: 2.0.29
    • OS: Windows 10.0.22621 (win32 x64)
    • Terminal: Windows Terminal
    • Shell: PowerShell
    • Install: npm global (@opencode/[email protected], channel latest)
    • No custom plugins, no HTTP_PROXY/HTTPS_PROXY env vars

    Observed behavior

    In normal mode (opencode from project directory):

    • TUI shows "Restarting service... Your session will resume automatically." for a few seconds
    • Then it disappears and the chat becomes usable

    In standalone mode (opencode --standalone):

    • TUI shows "Connection lost... Reconnecting to the server automatically." and stays stuck much longer

    Logs

    Same Timed out connecting to server loop on /api/event:

    event stream connecting    component=client attempt=0 role=cli
    event stream disconnected  component=client attempt=1 error="Timed out connecting to server" role=cli
    event stream connecting    component=client attempt=1 role=cli
    event stream disconnected  component=client attempt=2 error="Timed out connecting to server" role=cli
    ...
    event stream connected     component=client role=cli
    

    Server logs show /api/event returning HTTP 200, but the client keeps timing out.

    What I already tried

    • Restarted the background service (opencode service restart)
    • Removed the stale session tab from ~/.local/state/opencode/latest/tui/tabs.json
    • Confirmed no proxy variables are set

    The popup delay is shorter after cleaning the TUI state, but the underlying SSE first-frame timeout on Windows is still present — exactly as described in this issue.

  6. AcSsalazar commented on Oct 15, 2026

    @AcSsalazar
    Author

    Confirmed: this is Kaspersky, independent of OpenCode.

    Isolated test with a minimal Node HTTP SSE server (no OpenCode involved), listening on 127.0.0.1 and writing one SSE frame immediately:

    • GET /sse without Accept: text/event-stream → curl receives no response headers at all within 8 s (http_code=000).
    • GET /sse with Accept: text/event-stream → first byte in ~0.003 s (http_code=200).

    So Kaspersky Endpoint Security for Windows (KES 14.1.0.423) buffers loopback text/event-stream responses whenever the request lacks Accept: text/event-stream — for any server, not just OpenCode. This is exactly #48771.

    The OpenCode TUI simply doesn't send that header on /api/event, so on machines with Kaspersky the first frame is held past the ~2 s deadline and the client loops.

    Workarounds: exclude localhost/127.0.0.1 (or opencode.exe) from Kaspersky's local traffic inspection, or have the client send Accept: text/event-stream on the event subscription.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions