Visitar URL original
core: RPC dispatch races plugin registration on first start after update · Issue #63280 · anomalyco/opencode · GitHub
Skip to content

core: RPC dispatch races plugin registration on first start after update #63280

Open

Description

@syn7xx

Description

After an in-place auto-update (2.2.6 → 2.2.7), the first RPC request to a TUI plugin's registered domain fails with HTTP 400 rpc.unavailable for that domain, because the request that boots the location is dispatched on an RPC domain that is not registered yet — the plugin for that location is loaded only afterwards.

The client does not retry, so a TUI plugin reading its state on mount gets a hard failure on first render and keeps showing "unavailable" until the app is restarted.

This is not the location-scoping issue from #58438: here the location is resolved by the request itself (the request triggers location services booted), but RPC dispatch happens before the plugin's ctx.rpc.register runs. It is also the same shape as #63253 (one 400 on cold start, never retried), in a different subsystem.

Observed ordering in opencode.log (one process, serve --service):

06:09:26.666  background service starting   reason=version-mismatch previousVersion=2.2.6
06:09:27.073  loading plugin                (location A)
06:09:29.323  location services booted      (location B)   http.span=18
06:09:29.324  POST /api/rpc/<domain>/info -> 400           http.span=19
06:09:29.335  loading plugin                (location B)   <-- registered 11 ms LATER

Response body is the standard unavailable-domain error:

{"_tag":"RpcError","type":"rpc.unavailable","message":"RPC is unavailable: <domain>"}
HTTP 400 Bad Request

Control check in the same process right after the location settled — same endpoint, same domain:

{"output":{"version":"…","healthy":true,"status":"ready","httpEnabled":true,"reused":true}}

So registration does happen and the endpoint works; only the first, racing request fails.

Plugins

One local plugin (server + TUI entries) that registers an RPC domain via ctx.rpc.register(contract, handlers) in its server setup and calls it from a TUI contribution (ctx.client.rpc(contract).info() on mount of a home.footer.status slot and of a plugin page). Plus a small hook-only plugin.

Reproducible with any plugin that registers an RPC domain and reads it immediately from a TUI contribution.

OpenCode version

2.2.7 (upgraded in place from 2.2.6 by the built-in updater; channel latest, local=false)

Steps to reproduce

  1. Install a plugin whose server part calls ctx.rpc.register(contract, handlers) and whose TUI part calls ctx.client.rpc(contract).info() on mount.
  2. Run an older OpenCode version and let the built-in updater install a new one, so the CLI restarts the background service (reason=version-mismatch).
  3. Let the restarted service boot and render home (the TUI slot fires info immediately).
  4. Observe POST /api/rpc/<domain>/info → 400 rpc.unavailable, then loading plugin for that same location a few ms later.

Frequency: happens on the first service start after an update. A plain restart of an already-updated binary does not reproduce, which is why it reads as "broken after update, fixed by reopening".

Screenshot

Not applicable — the evidence is the log ordering above.

Operating System

macOS 26 (Darwin 25.2.0, arm64)

Terminal

iTerm2 (TERM_PROGRAM=iTerm.app, TERM=xterm-256color, COLORTERM=truecolor), zsh

Expected behavior

Any one of:

  • RPC dispatch for a location that is still booting waits until plugin setup completes, or
  • the response distinguishes "domain not registered yet" from "unknown domain" (e.g. a distinct retryable code / Retry-After), or
  • plugin RPC domains are registered before the location starts serving traffic.

Actual behavior

The first RPC call races plugin registration and gets a plain rpc.unavailable 400 that is indistinguishable from a permanently unknown domain, and the host never retries it.

Impact and current workaround

Fixed on the plugin side: system-level errors (rpc.*) are retried with backoff (300/700/1500/3000 ms) and the UI shows a neutral "Starting…" state while retrying; application-level errors are not retried because they are deterministic.

That masks the symptom, but the race remains: any plugin without its own retry will show a broken UI after an update. Ideally the host gates RPC dispatch on plugin readiness, or returns a retryable signal for a domain that is still registering.

Activity

  1. opencodejr commented on Nov 11, 2026

    @opencodejr

    Thanks for the detailed report and the log ordering, it made this easy to trace.

    The behaviour you're seeing comes from a deliberate change in 2.2.7. Up to 2.2.6 the RPC HTTP handler waited for plugin activation before dispatching. #62796 removed that wait: server handlers, including rpc.call, now answer with whatever is loaded at that moment. Clients are expected to retry, or to refresh on the plugin.updated event, if they call before plugins have loaded. That's why the first call after the service restarts can hit rpc.unavailable while a moment later it works. It isn't specific to updates: any request that reaches a location before its plugins finish loading can hit it.

    Your request, as I understand it: let clients tell "this RPC domain hasn't been registered yet" apart from "unknown domain". That could be a distinct retryable error, a readiness event, or a helper on ctx.client.rpc(...) that waits for the domain. Ideally the RPC docs would also mention this, since a TUI plugin that reads on mount will hit it. I've passed this to maintainers as a feature request. I can't promise if or when it will happen.

    In the meantime, your retry-with-backoff approach is in line with how the host expects clients to behave. Another option is to re-run your info() read when the plugin.updated event arrives.

    Related:

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