Skip to content

fix(desktop): preserve CLI service channels - #60683

Open
jerome-benoit wants to merge 8 commits into
anomalyco:devfrom
jerome-benoit:nix-desktop-service
Open

jerome-benoit wants to merge 8 commits into
anomalyco:devfrom
jerome-benoit:nix-desktop-service

Conversation

@jerome-benoit

@jerome-benoit jerome-benoit commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #60676

Type of change

  • Bug fix
  • Documentation

What does this PR do?

Keep Desktop service discovery aligned with the bundled CLI's build channel. Official latest builds use service.json; Nix prod builds use service-prod.json. Nix Desktop also requires the dependency-packaging changes in #59403.

  • Share the registration filename rule between CLI and Client.
  • Use one path for discovery, connection and reconnection; reject early probes from another state directory.
  • Require an explicit channel for prebuilt CLIs; use the selected channel for downloads.
  • Separate staged executables by version, architecture and channel; retain possible owners of adopted services.

How did you verify your code works?

  • Current head 6b0567d: full Desktop package suite 578 passed / 0 failed on the base runner script; root bun run check passes (lint with warnings, 34/34 typecheck tasks).
  • Boot probe is consumed before CLI preparation and a probe from another state directory is rejected; the reviewer reproduction re-ran on the merged tree and passes (Desktop 46 service tests, client 8, tsc). Earlier review rounds: 28 service/build-policy tests repeated ten times passed, and the missing/empty prebuilt-channel cases fail before the fix and pass after it.
  • Linux x86_64: built with an unknown channel, downloaded the dev CLI, rendered the Desktop home on Xvfb and confirmed authenticated API discovery.
  • Electron: same-version latest-to-prod staging, repeated-stage reuse and legacy-stage isolation passed. The staged prod command ran CLI source with the prod define; authenticated API and Service.ensure passed.

Combined Nix runtime verification covers 122ead5 + d8dde3e, not the current Desktop head. No fresh combined Nix build was run. Windows/ARM/macOS runtimes are untested. Port-change recovery requires a renderer reload; baseline regression status is unverified.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #52121, #59601

Public API: adds registrationFilename() and an optional channel argument to fallback() in @opencode/client/service (backwards compatible)

Test User added 2 commits October 30, 2026 19:32
Preserve the raw CLI channel for service registration while normalizing
only the Desktop app identity. Keep existing prod configuration and state.

Closes anomalyco#60676
@jerome-benoit

jerome-benoit commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Nix integration verification (122ead5 + d8dde3e)

These results cover only the pinned commits below, not newer Desktop revisions.

git fetch https://github.com/jerome-benoit/opencode.git nix-desktop-service nix-lockfile-deps
git worktree add --detach /tmp/opencode-desktop-check d8dde3eb08c794e9bf2a90537aa31cb83558aa45
git -C /tmp/opencode-desktop-check merge --no-edit 122ead50368eb2d1a4a9e4617b56f9775cd4e8ce
cd /tmp/opencode-desktop-check
nix build .#opencode .#opencode-desktop --no-link --print-out-paths -L

The observed local merge was 0887daf2ab7717d4e2afd45b6a8c284f1ca75153, producing version 2.2.4+0887daf with Bun 1.4.2. A new merge produces its own revision suffix.

Runtime probes used private HOME/XDG/database paths and OPENCODE_DESKTOP_TEST_ROOT. Final GUI verification ran on Xvfb with --no-sandbox --disable-gpu; native GPU acceleration is not validated. No external PTY helper override was set.

The final embedded shell produced NIX_FINAL_0887_OK. The separate input probe returned ANSWER:verified-input. During the older-service upgrade, the server PID changed from 103765 to 139625 while terminal PID 136279 remained unchanged. All owned test applications, services and the virtual display were stopped afterward.

@jerome-benoit
jerome-benoit marked this pull request as ready for review October 30, 2026 21:14

@opencodejr opencodejr 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.

The fix works from source. I started a prod-channel CLI (OPENCODE_CHANNEL="prod", serve --service) with isolated XDG directories, then looked the service up the way Desktop does. On the base nothing is found, and fallback() always points at service.json. With this PR, fallback("prod") resolves to service-prod.json and Service.discover({ file }) finds the running service. latest, dev, beta and next still map to service.json, so official builds keep the same registration file. The Electron app itself was not built or launched.

The nearby desktop, client and CLI service tests pass. tsc passes for all three packages.

Public API: @opencode/client adds registrationFilename(channel). fallback() gains an optional channel argument that defaults to latest, so existing callers are unaffected. Both changes are additive and backwards compatible. Desktop's staged CLI directories under userData are now named <version>-<arch>-<channel>, so each user re-stages once after upgrading. Legacy stages that an adopted service may still run from are kept.

One non-blocking point is noted inline. With a prebuilt CLI and no OPENCODE_CHANNEL, Desktop assumes dev, but the CLI build takes its channel from the git branch name.

Related: #52121, #59601

Comment thread packages/desktop/script/utils.ts Outdated
}

export function resolveCliChannel(built: boolean) {
return built ? process.env.OPENCODE_CHANNEL || "dev" : resolveChannel()

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.

When OPENCODE_CHANNEL is unset, this assumes the prebuilt CLI is dev. packages/script builds the CLI with the git branch name as its channel instead (or latest when a release OPENCODE_VERSION is set). On a feature branch the CLI then registers service-<branch>.json, while Desktop looks in service.json, which is the same mismatch this PR fixes for Nix. The base has the same gap, so this isn't a regression. It would hold up better to read the channel from the built CLI's own metadata, or to fail when packages/cli/dist is used without an explicit OPENCODE_CHANNEL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Set OPENCODE_CHANNEL explicitly to the value used to build a prebuilt CLI. Missing or empty values are rejected before resource preparation. Downloaded CLIs retain the dev/beta fallback; explicit custom channels remain unchanged.

The rejection cases fail before the fix and pass after it. Desktop service/build-policy tests: 280 passed across ten runs. The real build entrypoint was exercised in an isolated workspace: both invalid cases stop before any resources or output are created.

Implementation: 36cfa45.

Prebuilt CLI manifests do not record their channel. Reject missing
OPENCODE_CHANNEL before preparing Desktop build resources.

@opencodejr opencodejr 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.

The new commit fixes my earlier point. A Desktop build with a prebuilt CLI now fails when OPENCODE_CHANNEL is unset or empty, instead of assuming dev. The new test cases cover this, and the Desktop tests (28) and tsc -b pass. The release workflow and the Nix build both set the channel, so neither should hit the new error. The discovery fix itself is unchanged since my last review. Public API: nothing new beyond the backwards-compatible client additions noted before. No new concerns.

@opencodejr opencodejr 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.

The merge with upstream dev keeps the fix. I re-ran the from-source reproduction: a prod-channel CLI registers service-prod.json, and Desktop's lookup now finds it (latest still maps to service.json). The Desktop (46) and client (8) tests and tsc pass. My earlier point about prebuilt CLIs without OPENCODE_CHANNEL was fixed in 36cfa45, and the merge doesn't change that. Public API: no change since the last review. One small new point from the merge is noted inline.

  • packages/desktop/src/main/service/background-service.ts:49: Upstream took the probe before prepare() so that the result is used only once. Now prepare() (which resolves the CLI and can fail, e.g. a stage copy blocked by antivirus) runs first. If it fails, takeSidecarProbe is never called and the boot-time probe stays stored. The next initial attempt can then adopt a service that may have exited since launch, which is what the comment on takeSidecarProbe is meant to prevent. The check that follows would still replace a dead service, so this is minor. Taking the probe first and comparing its file after prepare() would keep both properties.

@jerome-benoit

Copy link
Copy Markdown
Contributor Author

Fixed in 9077bab. takeSidecarProbe() now runs before prepare(), so the boot probe is always consumed exactly once. The probe's service file path is then compared with the CLI path resolved after staging, so a probe from a different state directory is never adopted — both properties are kept. Regression test added in packages/desktop/src/main/service/sidecar-probe.test.ts: probe consumed once; a second initial attempt with a service registered under another state directory is rejected. Desktop package suite: 575 passed / 0 failed; root lint and all 34 typecheck tasks pass.

@opencodejr opencodejr 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.

The new commit fixes my earlier point about the probe order: the boot-time probe is now taken before the CLI is prepared, and the state-directory check still applies. bun run test in packages/desktop passes, as does tsc -b. Public API: no change since the last review.

The new test is worth reworking. It replaces electron and @opencode/client/service for the whole test process. Without the new --isolate flag, those replacements leak into other test files and break 3 of them. Details are inline.

Object.defineProperty(process, "resourcesPath", { value: resources, configurable: true })
writeFileSync(path.join(resources, "opencode-cli.version"), "1.0.0\n")

mock.module("electron", () => ({

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.

mock.module("electron") and mock.module("@opencode/client/service") apply to the whole test process, and the file also overrides process.resourcesPath at module scope. That's why package.json now runs the entire Desktop suite with --isolate. Without it, bun test --timeout 30000 fails 3 tests in other files ("Missing 'default' export in module .../electron/index.js"). With this file removed, the suite passes without --isolate. The test only checks that takeSidecarProbe() returns its result once, which the five-line function already shows. Please drop this test and the --isolate change so the fix stays small and the test runner stays as it was.

@jerome-benoit

Copy link
Copy Markdown
Contributor Author

Dropped in ae2858f, per the inline point on sidecar-probe.test.ts. The mock.module("electron") harness leaked into the package-wide module registry, so the suite needed --isolate; the runner change outgrew what the test covered. The once-consumption it asserted is visible in the five-line takeSidecarProbe. Testing the ordering in BackgroundService.probe itself would mean mocking Electron, CLI staging and ensure/adopt with no existing harness at that boundary, so the ordering stays documented by the code comment instead. Suite without the file and flag: 578 passed / 0 failed in 4s; root lint and all 34 typecheck tasks pass.

@opencodejr opencodejr 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.

The new commit removes the mocked probe test, and the Desktop suite now passes without --isolate (bun test --timeout 30000: 0 failures). The --isolate flag that was added for that test is still in the Desktop test script in packages/desktop/package.json. Please revert it so the test runner stays as it was. Public API: no change since the last review.

@opencodejr opencodejr 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.

The Desktop test script is back to the base command (no --isolate), and bun run test in packages/desktop passes (0 failures). Both of my earlier points are now addressed. Public API: no change since the last review. No new concerns.

@jerome-benoit

Copy link
Copy Markdown
Contributor Author

Correction on my previous comment: commit ae2858f dropped only the test file; the --isolate runner flag removal landed separately in 6b0567d. The PR now carries no packages/desktop/package.json diff at all.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nix Desktop cannot discover its bundled CLI service

1 participant