Repository navigation
Description
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 RequestControl 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
- Install a plugin whose server part calls
ctx.rpc.register(contract, handlers)and whose TUI part callsctx.client.rpc(contract).info()on mount. - 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). - Let the restarted service boot and render home (the TUI slot fires
infoimmediately). - Observe
POST /api/rpc/<domain>/info→400 rpc.unavailable, thenloading pluginfor 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
opencodejr commentedon Nov 11, 2026
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 theplugin.updatedevent, if they call before plugins have loaded. That's why the first call after the service restarts can hitrpc.unavailablewhile 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 theplugin.updatedevent arrives.Related:
rpc.unavailablefrom a plugin RPC, but a different cause (location scoping)