Skip to content

cli: service status reports stopped for healthy mismatched daemon #36274

Description

@kitlangton

Summary

opencode2 service status prints stopped when the registration points to a healthy managed daemon running an older version than the installed CLI. The daemon is not stopped: opencode2 api get /api/health can connect to it because the API command intentionally ignores managed-service version mismatches.

This makes a normal deferred-update state look like a missing or corrupt service and obscures which process clients are actually using.

Environment

  • opencode version: reproduced across 0.0.0-next-15258 → next-15277 and next-15293 → next-15295
  • OS: macOS 25.5.0 (arm64)
  • Terminal: WarpTerminal, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/zsh
  • Install/channel: global Bun installation, next
  • Active plugins: share-with-team.ts, debug-workspace-plugin.ts, @warp-dot-dev/opencode-warp (not involved in service discovery)

Reproduction

  1. Start a managed service on version A.
  2. Allow the updater to install version B without launching a new normal client.
  3. Confirm opencode2 --version reports version B.
  4. Run opencode2 service status.
  5. Run opencode2 api get /api/health.

The deterministic regression test uses a real local HTTP health endpoint and a service registration whose healthy server version differs from the CLI:

Expected status output to contain: http://127.0.0.1:<port>/
Received: stopped

Expected Behavior

service status should distinguish these states:

{
  "status": "running",
  "url": "http://127.0.0.1:4096",
  "pid": 81804,
  "version": "0.0.0-next-15293",
  "compatible": false,
  "clientVersion": "0.0.0-next-15295"
}

Actual Behavior

The status command applies the current CLI's exact version gate. A healthy older service therefore becomes indistinguishable from no service:

$ opencode2 --version
opencode2 v0.0.0-next-15295

$ opencode2 service status
stopped

$ opencode2 api get /api/health
{"healthy":true,"version":"0.0.0-next-15293","pid":81804}

Additional Context

Lifecycle

sequenceDiagram
  participant Old as Running daemon A
  participant Disk as Global installation
  participant Status as service status
  participant API as api get
  participant Client as Normal client B
  Old->>Disk: Install version B
  Note over Old: Process image remains version A
  Status->>Old: Probe with exact version B
  Old-->>Status: Healthy version A
  Status-->>Status: Print stopped
  API->>Old: Probe without version gate
  Old-->>API: Healthy version A
  Client->>Old: Replace mismatched daemon
  Client->>Disk: Start daemon B
Loading

Installing an update does not replace an already-running process image. Existing clients may continue using daemon A. A later normal client uses the default mismatch: "replace" policy, terminates daemon A, and starts daemon B. That deferred replacement is working; the incorrect part is the status projection during the mismatch window.

Timestamped log

The original incident:

2026-07-10T02:36:23Z  managed daemon starts on next-15258 (run=34241966)
2026-07-10T13:32:36Z  daemon installs next-15277; running process remains next-15258
2026-07-10T13:42:37Z  old daemon installs next-15277 again
2026-07-10T13:47:41Z  installed CLI reports next-15277
2026-07-10T13:47:41Z  service status runs and prints stopped
2026-07-10T13:47:42Z  api health reaches the healthy next-15258 daemon
2026-07-10T13:49:02Z  normal next-15277 TUI starts
2026-07-10T13:49:03Z  next-15258 daemon begins shutting down its location graphs
2026-07-10T13:49:04Z  next-15277 service contender starts

Independent reproduction:

2026-07-10T15:43:31Z  managed daemon next-15293 starts (run=8d63fdf7, PID 81804)
2026-07-10T15:46:27Z  updater sees next-15295
2026-07-10T15:46:30Z  updater installs next-15295
2026-07-10T15:46:38Z  installed CLI reports next-15295; service status prints stopped
2026-07-10T15:46:39Z  api health reports next-15293, PID 81804
2026-07-10T15:46:42Z  normal next-15295 client starts
2026-07-10T15:46:43Z  next-15293 daemon shuts down
2026-07-10T15:46:44Z  next-15295 daemon starts (run=b72556c5, PID 86661)
2026-07-10T15:47:36Z  service status reports the URL; api health reports next-15295

Source path

  • ServiceConfig.options() supplies the current InstallationVersion to discovery.
  • Service.discover(options) rejects registrations and health responses with a different version.
  • The status handler maps an unsuccessful exact-version discovery directly to stopped.
  • The API handler deliberately resolves with mismatch: "ignore", which removes the version gate and reuses the healthy old daemon.
  • Normal clients default to mismatch: "replace", which stops the old daemon and starts the installed version.
  • Registration remains coherent throughout; this is not service-election corruption.

Proposed fix

Add a read-only service status result that probes a healthy registration without applying the compatibility gate, then emit an explicit JSON status with server identity and compatibility. Keep exact-version discovery and replacement behavior unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions