Skip to content

cli: managed reconnect waits until fourth failure to ensure service #36581

Description

@kitlangton

Summary

When a managed background service becomes unavailable, a connected V2 TUI performs three passive discovery retries before it is allowed to ensure that a service is running. Each event-stream attempt can spend two seconds waiting for its first event, so the TUI can remain on an opaque Connection lost / Attempt 3 / Transport screen even though the managed service recovery operation is idempotent and safe to run immediately.

This reproduced on the current next implementation during an update/restart sequence. One affected TUI took about 27 seconds to recover. The delayed escalation also synchronized many connected TUIs onto the same restart boundary, amplifying the reconnect herd already tracked in #36285.

Environment

  • opencode version: 0.0.0-next-15360
  • OS: macOS 25.5.0, Darwin 25.5.0, arm64
  • Terminal: WarpTerminal 0.2026.07.01.09.21.stable_01; TERM=xterm-256color; COLORTERM=truecolor
  • Shell: /bin/zsh
  • Install/channel: global Bun install, next
  • Active plugins: ~/.config/opencode/plugins/share-with-team.ts, @warp-dot-dev/opencode-warp, and the repository debug workspace plugin

Reproduction

  1. Keep several V2 TUIs connected to the shared managed background service.
  2. Stop or replace the service, for example during an automatic next update or with opencode2 service restart.
  3. Observe a connected TUI's event-stream recovery.
  4. Inspect the TUI overlay and ~/.local/share/opencode/log/opencode.log.

This was observed during an automatic update/restart sequence and is deterministic from the current reconnect threshold whenever no healthy registration is discoverable during attempts 1 through 3.

Expected Behavior

On the first failed managed-service reconnect, the client should immediately perform an idempotent, version-agnostic ensure-running operation:

  • Reuse a healthy registered service at any version.
  • Start a service when none is healthy.
  • Never replace a healthy service because a passive reconnecting client has a different version.
  • Converge all reconnecting clients on the elected service without waiting for a fourth event-stream failure.

The TUI should describe the managed-service recovery action instead of exposing the internal error category Transport by itself.

Actual Behavior

packages/cli/src/services/server.ts gives passive reconnects three attempts before calling Service.start:

reconnect: (attempt) =>
  Effect.runPromise(
    Effect.gen(function* () {
      if (attempt > 3) return yield* Service.start(reconnectOptions)
      const endpoint = yield* Service.discover(reconnectOptions)
      if (endpoint !== undefined) return endpoint
      return yield* Effect.fail(new Error("Background server is unavailable"))
    }).pipe(Effect.provide(NodeFileSystem.layer)),
  )

reconnectOptions already clears the version constraint:

const reconnectOptions = { ...options, version: undefined }

Service.start(reconnectOptions) is an idempotent ensure-running operation: it first performs health-checked discovery and returns a healthy endpoint. With version: undefined, it does not replace a healthy service due to a client/server version mismatch. The three-attempt delay therefore does not protect the service from replacement; it only delays recovery.

The affected TUI timeline on 2026-07-12, UTC:

16:06:34.227  TUI starts on next-15360
16:06:35.059  existing event streams disconnect
16:06:37.706  one replacement serve --service process starts
16:06:50.284  affected TUI event stream attempt 0 begins
16:06:52.287  attempt 0 times out; displayed attempt becomes 1
16:06:55.299  attempt 1 begins
16:06:57.304  attempt 1 times out; displayed attempt becomes 2
16:07:00.321  attempt 2 begins
16:07:02.322  attempt 2 times out; displayed attempt becomes 3
16:07:05.336  attempt 3 begins
16:07:07.347  attempt 3 times out; displayed attempt becomes 4
16:07:12.454  another serve --service process starts
16:07:17.107  attempt 4 begins
16:07:17.215  event stream connects

The screenshot was captured while the TUI displayed:

Connection lost
Reconnecting to server...
Attempt 3
Transport

Additional Context

Proposed minimal fix

For managed-service reconnects, call the idempotent version-agnostic ensure operation immediately:

reconnect: () =>
  Effect.runPromise(
    Service.start(reconnectOptions).pipe(Effect.provide(NodeFileSystem.layer)),
  )

Changing attempt > 3 to attempt >= 3 is not sufficient. It retains the delayed-recovery policy and only removes one retry.

Acceptance criteria

  • The first failed event-stream reconnect invokes managed-service ensure-running behavior.
  • A healthy registered service is reused regardless of version during passive reconnect.
  • A missing or unhealthy service is started without waiting for a fourth stream failure.
  • Concurrent reconnecting clients converge through service election; only the elected service remains active.
  • A regression test covers a stale/unreachable original endpoint followed by a newly registered endpoint.
  • A regression test proves passive reconnect does not replace a healthy version-mismatched service.
  • Connection diagnostics distinguish service discovery/startup failure from the generic generated-client error category Transport.

Related issues

Out of scope

  • Coordinating automatic update activation with active clients and executions.
  • Further optimization of cold Location graph startup.
  • MCP connection rate limiting during a cold-start herd.
  • Durable recovery of in-flight provider work after process termination.

Those remain valid parts of #36285 but are not required to remove this deterministic reconnect delay.

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions