Skip to content

fix(server): discover channel-suffixed service registrations - #338

Open
taekop wants to merge 2 commits into
nickjvandyke:mainfrom
taekop:fix/discover-channel-service-file
Open

taekop wants to merge 2 commits into
nickjvandyke:mainfrom
taekop:fix/discover-channel-service-file

Conversation

@taekop

@taekop taekop commented Oct 7, 2026

Copy link
Copy Markdown

Tasks

  • I read CONTRIBUTING.md
  • I ensured my changes pass automated checks
  • I reviewed and understand the AI-generated code in my changes (if any)

Description

OpenCode v2 writes service-<channel>.json instead of service.json for channels other than latest/dev/beta/next (filename() in packages/cli/src/services/service-config.ts). The Nix package uses prod, so discovery only looking at service.json never found the server.

registration() now lists service.json and service-*.json in the state directory, newest first, and returns the first one with a valid url, as suggested in the issue. Symlinked files are followed as before, and on an mtime tie service.json wins. Behaviour with only service.json is unchanged. Also updated the doc comments and AGENTS.md mentions of service.json.

Testing

  1. Put a registration at $XDG_STATE_HOME/opencode/service-prod.json (no service.json).
  2. :checkhealth opencode. On main: "No OpenCode background service registered yet". With this change: "OpenCode background service (v2.0.18) registered at http://127.0.0.1:49374".
  3. Called require("opencode.server.discovery").registration() headless against these state dirs. Before/after:
    • empty dir, missing dir, empty service.json, only other.json/servicex.json, only service-prod.json.tmp, a directory named service-x.json: nil both before and after
    • only service.json: same result both before and after
    • only service-prod.json / service-local.json / service-my[chan]-1.json: nil before, now found
    • both files: the newer one wins; an invalid or url-less newer file is skipped in favour of the older valid one

stylua --check . and lua-language-server --check=. (the CI config, with library paths pointed at local Neovim 0.12.5 runtime, snacks.nvim and blink.cmp) report no problems.

Related Issue(s)

Fixes #336

Screenshots/Videos

N/A

Repository owner deleted a comment from github-actions Bot Oct 19, 2026
Repository owner deleted a comment from github-actions Bot Oct 19, 2026
@nickjvandyke

Copy link
Copy Markdown
Owner

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this is a focused, well-tested fix and the description explains the "why" clearly. CI (stylua + LuaLS) is green and the contributor checklist is complete. I don't see any blocking issues.

A few non-blocking thoughts (suggestions only — the human can decide):

  1. Channel selection / stale registrations. Picking the newest parseable registration is a reasonable heuristic and matches the issue's suggestion. However, when multiple channels share the state dir, a newer-but-stale service-<channel>.json (server since exited) will shadow an older live service.json. find() only tries registration()'s single result, then falls through to server.start instead of using the live registration. If you want to harden this, expose the ordered candidates and have find() try each until one connects, or prefer plain service.json first and only fall back to channel files. Fine as a follow-up; not a blocker for this fix.

  2. mtime.nsec (nit). stat.mtime.nsec can be absent on older libuv/luv builds. In practice all stats in one process come from the same library, so nil/number mixing is unlikely, but (a.mtime.nsec or 0) / (b.mtime.nsec or 0) would make the comparator obviously safe.

  3. The ^service%-.+%.json$ pattern deliberately (and correctly) rejects servicex.json / service-prod.json.tmp, and fs_stat + type == "file" handles symlinks and service-*.json directories as described. Nice.

Skipping invalid/URL-less newer files in favor of older valid ones, and keeping the single-service.json behavior unchanged, both look correct.

Repository owner deleted a comment from github-actions Bot Oct 19, 2026
@nickjvandyke

Copy link
Copy Markdown
Owner

Thanks for your contribution @taekop ! It generally LGTM. I agree with the risk of stale registration files though. Sequentially attempting to connect to each discovered registration in find() seems like a reasonable safety to me. What do you think?

OpenCode v2 names the registration file service-<channel>.json for
channels other than latest/dev/beta/next (e.g. Nix builds use prod), so
only reading service.json missed those servers. Read the newest valid
service.json or service-*.json in the state directory instead.

Fixes nickjvandyke#336
A newer registration left behind by an exited server (e.g. a stale
service-prod.json) shadowed an older live service.json, so find() fell
through to starting a server. Return all valid registrations newest
first and try them in order.
@taekop
taekop force-pushed the fix/discover-channel-service-file branch from ca959ab to 60fd038 Compare October 20, 2026 09:12
@taekop

taekop commented Oct 20, 2026

Copy link
Copy Markdown
Author

Agreed, done in 60fd038: discovery now returns every valid registration newest first (registrations(); registration() still returns the first, for :checkhealth), and find() tries each in order before falling back to server.start. I checked it headlessly against a stub /api/info server: a stale newer service-prod.json now falls through to the live older service.json, all-stale still starts a server, and symlinked registrations still resolve. I also applied the bot's mtime.nsec or 0 nit and rebased on main.

@nickjvandyke

Copy link
Copy Markdown
Owner

Nice! Cool solution with try, I've never seen a recursive promise before!

imo we can remove M.registration() and inline the registrations()[1] in the health check for a bit less noise, if that sounds good to you.

Then LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: v2 service discovery misses Nix-packaged OpenCode (writes service-prod.json, plugin reads service.json)

2 participants