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
- Keep several V2 TUIs connected to the shared managed background service.
- Stop or replace the service, for example during an automatic
next update or with opencode2 service restart.
- Observe a connected TUI's event-stream recovery.
- 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.
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 / Transportscreen even though the managed service recovery operation is idempotent and safe to run immediately.This reproduced on the current
nextimplementation 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
0.0.0-next-153600.2026.07.01.09.21.stable_01;TERM=xterm-256color;COLORTERM=truecolor/bin/zshnext~/.config/opencode/plugins/share-with-team.ts,@warp-dot-dev/opencode-warp, and the repository debug workspace pluginReproduction
nextupdate or withopencode2 service restart.~/.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:
The TUI should describe the managed-service recovery action instead of exposing the internal error category
Transportby itself.Actual Behavior
packages/cli/src/services/server.tsgives passive reconnects three attempts before callingService.start:reconnectOptionsalready clears the version constraint:Service.start(reconnectOptions)is an idempotent ensure-running operation: it first performs health-checked discovery and returns a healthy endpoint. Withversion: 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:
The screenshot was captured while the TUI displayed:
Additional Context
Proposed minimal fix
For managed-service reconnects, call the idempotent version-agnostic ensure operation immediately:
Changing
attempt > 3toattempt >= 3is not sufficient. It retains the delayed-recovery policy and only removes one retry.Acceptance criteria
Transport.Related issues
Out of scope
Those remain valid parts of #36285 but are not required to remove this deterministic reconnect delay.