Repository navigation
cli: managed reconnect waits until fourth failure to ensure service #36581
Description
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
nextupdate or withopencode2 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
- 2.0: managed-service restart causes reconnect herd and resource spikes #36285 remains the open umbrella for managed-service replacement, reconnect herds, cold-location startup, and resource spikes.
- V2: reconnecting clients can restart-loop the shared daemon #35448 covered restart loops caused by passive reconnect version replacement. It was closed after lifecycle changes; this report preserves its required rule that passive reconnect has no version-replacement authority.
- fix(cli): recover from concurrent service replacement during startup #35801 covered transport loss during initial hydration and was closed with a request for a fresh focused issue if behavior remained reproducible.
- fix(cli): converge service election after restart #35964 covered service election convergence and was closed with the same request.
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.
Activity
armancharan commentedon Jul 17, 2026
Closing the loop: the reconnect policy itself already landed with the managed service lifecycle work. #37570 adds the acceptance-criteria regression tests (ensure on first failure + no version replacement on passive reconnect) so this issue can close cleanly.
neriousy commentedon Oct 21, 2026
Done in #36895: managed reconnect now calls Service.ensure without a version constraint on the first failure (packages/cli/src/services/server-connection.ts), so it no longer waits for a fourth event-stream failure.