Skip to content

Nix Desktop cannot discover its bundled CLI service #60676

Open

Description

@jerome-benoit

Description

The Nix Desktop build starts its bundled CLI but cannot connect to it. The backend responds to /api/info with HTTP 200 while Desktop reports Timed out waiting for the background service to start.

Nix builds the CLI with OPENCODE_CHANNEL=prod, which writes service-prod.json. Desktop probes service.json. Its build normalizes both latest and prod to the production app identity, losing the CLI channel needed for service discovery.

Desktop should discover the bundled CLI's registration and preserve existing configuration and state. Official latest builds must continue using service.json.

OpenCode version

2.2.3+122ead5

Steps to reproduce

  1. Build opencode-desktop at jerome-benoit/opencode@122ead50368eb2d1a4a9e4617b56f9775cd4e8ce.
  2. Launch the packaged app with isolated HOME/XDG directories.
  3. Observe the startup timeout; the private backend is healthy and only service-prod.json exists.

Build and runtime evidence: #59403 (comment)

Operating System

Linux x86_64

Activity

  1. opencodejr commented on Oct 30, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced the discovery mismatch from source, without building the Nix Desktop package itself:

    • I ran the CLI as a prod-channel build (OPENCODE_CHANNEL="prod", the same channel nix/opencode.nix uses) with serve --service and empty HOME/XDG directories. It registered only service-prod.json, and /api/info returned HTTP 200.
    • I then looked up the service the way Desktop does: Service.discover({ version }) with no registration file (sidecar-probe.ts, background-service.ts). It found nothing, because it only reads service.json. Pointing it at service-prod.json found the service right away.

    The results are the same on v2.2.3 and on the latest dev (6700dd8). The script below reproduces it.

    Related: #59403 (your Nix PR, which notes this timeout), #52121 and #52122.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #60676: a CLI built with OPENCODE_CHANNEL=prod (as nix/opencode.nix does) registers its
    # background service in service-prod.json, while Desktop's service lookup (Service.discover / ensure
    # with no `file`, as in packages/desktop/src/main/service/{sidecar-probe,background-service}.ts) only
    # reads service.json, so it never finds the healthy service.
    # Usage: REPO=/workspace/repo bash repro.sh
    set -u
    REPO=${REPO:-/workspace/repo}
    ROOT=$(mktemp -d /tmp/opencode/nix-svc.XXXX)
    export HOME=$ROOT/home XDG_STATE_HOME=$ROOT/state XDG_CONFIG_HOME=$ROOT/config XDG_DATA_HOME=$ROOT/data XDG_CACHE_HOME=$ROOT/cache
    mkdir -p "$HOME" "$XDG_STATE_HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" "$ROOT/project"
    cd "$ROOT/project"
    
    # 1. Run the CLI as a channel=prod build (the same compile-time define nix/opencode.nix produces).
    bun --conditions=browser \
      --define 'OPENCODE_CHANNEL="prod"' --define 'OPENCODE_VERSION="2.2.3"' \
      --preload="$REPO/packages/tui/node_modules/@opentui/solid/scripts/preload.js" \
      "$REPO/packages/cli/src/index.ts" serve --service >"$ROOT/serve.log" 2>&1 &
    PID=$!
    for i in $(seq 1 60); do ls "$XDG_STATE_HOME"/opencode/service*.json >/dev/null 2>&1 && break; sleep 1; done
    sleep 2
    
    echo "== registration files in \$XDG_STATE_HOME/opencode =="
    ls "$XDG_STATE_HOME/opencode" | grep -i service
    REG=$(ls "$XDG_STATE_HOME"/opencode/service*.json | head -1)
    URL=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('$REG','utf8')).url)")
    PW=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('$REG','utf8')).password ?? '')")
    echo "== backend health ($URL/api/info) =="
    curl -s -o /dev/null -w "HTTP %{http_code}\n" -u "opencode:$PW" "$URL/api/info"
    
    # 2. Look the service up exactly as Desktop does (no explicit registration file).
    cat >"$REPO/packages/desktop/.repro-discover.ts" <<'EOF'
    import { Service } from "@opencode/client/service"
    const found = await Service.discover({ version: "2.2.3" })
    console.log("Desktop-style Service.discover() ->", found ?? "undefined (service not found)")
    const withFile = await Service.discover({ version: "2.2.3", file: process.argv[2] })
    console.log("Service.discover({ file: service-prod.json }) ->", withFile?.url ?? "undefined")
    EOF
    echo "== Desktop-style discovery =="
    (cd "$REPO/packages/desktop" && bun .repro-discover.ts "$REG")
    rm -f "$REPO/packages/desktop/.repro-discover.ts"
    
    kill $PID 2>/dev/null; wait $PID 2>/dev/null
    pkill -f "$ROOT" 2>/dev/null

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions