Skip to content

fix(cli): isolate background service per config profile - #58300

Open
jskswamy wants to merge 1 commit into
anomalyco:devfrom
jskswamy:isolate-service-profiles
Open

jskswamy wants to merge 1 commit into
anomalyco:devfrom
jskswamy:isolate-service-profiles

Conversation

@jskswamy

@jskswamy jskswamy commented Oct 23, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #58141

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Previously, the managed background service registered a single per-user state file (~/.local/state/opencode/service.json) and bound a single default port (49374 / 0xc0de) regardless of OPENCODE_CONFIG_DIR. Launching a second profile with a different OPENCODE_CONFIG_DIR (and profile-specific environment variables) discovered and attached to the first profile's background server instead of starting its own.

This change:

  1. Detects when OPENCODE_CONFIG_DIR resolves to a non-default config directory (ServiceConfig.activeProfile()).
  2. Deterministically derives a profile-specific registration filename in global.state (service-<hash>.json / service-<channel>-<hash>.json) and a profile-specific default port (ServiceConfig.defaultPort) from the resolved OPENCODE_CONFIG_DIR path, while keeping the default profile (service.json and port 49374) unchanged.
  3. Canonicalizes OPENCODE_CONFIG_DIR before ServerProcess changes directory to $HOME and passes it in ServiceConfig.options().env so spawned background services and CLI commands (service start, status, restart, stop) consistently resolve the same profile.

How did you verify your code works?

  • Added unit tests in packages/cli/test/service.test.ts covering per-profile port derivation, registration filename derivation, and ServiceConfig.options() isolation in a shared state directory.
  • Added an end-to-end multi-profile CLI test in packages/cli/test/service.test.ts verifying that two OPENCODE_CONFIG_DIR profiles start on distinct ports, report independent service status, and stop independently without affecting each other.
  • Ran bun test test/service.test.ts and bun run check.

Screenshots / recordings

N/A

Checklist

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

Added by the opencode bot (AI-generated).

Related: #56708, #42690, #52891

Public API: with a non-default OPENCODE_CONFIG_DIR, the background service uses a per-profile registration file and a derived default port (existing registrations are migrated when the saved password matches)

@opencode-agent opencode-agent 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 for the scenario in #58141. Run from source, the base attaches two different OPENCODE_CONFIG_DIR profiles (and the default) to a single service. With this PR each profile gets its own service and port, and service stop on one leaves the other running. The new and changed service tests and tsc -b were run: the existing lifecycle failures also happen on the base, but the new end-to-end test failed here (see inline).

This is also a design change. The bot's reply on #58141 says one shared service per user is the documented v2 behaviour and marks the issue as a feature request, so maintainers should confirm that one service per config dir is the direction they want, rather than the server using the client's env or a warning on mismatch. A different XDG_DATA_HOME/XDG_STATE_HOME, which the issue also mentions, isn't handled.

Public API: no SDK, HTTP, plugin or config-schema changes. The behaviour around the OPENCODE_CONFIG_DIR environment variable does change:

  • Registration file: for any non-default config dir, $XDG_STATE_HOME/opencode/service.json → service-<hash>.json (service-<channel>.json → service-<channel>-<hash>.json).
  • Default port: 49374 (or the channel port) → a hash-derived port between 10000 and 59999.
  • Spawned service: its env now always includes OPENCODE_CONFIG_DIR.

This isn't backwards compatible for people who already use one custom OPENCODE_CONFIG_DIR for everything:

  • Their running service and its service.json are no longer found, because migration is skipped. It stays running on 49374, unreachable by service stop, and a second server starts.
  • Clients that rely on the default service.json, such as the promise client's fallback() in packages/client/src/service-probe.ts, won't see the profile's service.

The upgrade path needs handling (see inline).

Related: #56708, #42690, #52891

Comment thread packages/cli/test/service.test.ts Resolved
@jskswamy
jskswamy force-pushed the isolate-service-profiles branch from 3392451 to ab283d9 Compare October 23, 2026 07:02

@opencode-agent opencode-agent 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 addresses all three earlier points. I checked the upgrade case by starting a service with the base build under one custom OPENCODE_CONFIG_DIR, then running this branch. service status found the existing service through the moved registration, service stop stopped it, and the next service start used the profile's own port. A second profile still got its own service. Profile ports now stay in 10000–32767, below the Linux range for outgoing connections and the Windows remap. The flaky end-to-end test is gone. test/service.test.ts (26 pass) and tsc -b pass.

Public API: unchanged since the last review. When OPENCODE_CONFIG_DIR points to a non-default directory, the registration file becomes service[-<channel>]-<hash>.json and the default port is derived from the directory (10000–32767). An existing service.json registration is moved over when its password matches the profile's saved one, so existing single-profile setups keep their running service.

No new concerns.

Derive the managed service registration filename in global state and
the default service port from the resolved OPENCODE_CONFIG_DIR path
when a non-default config directory is active, and propagate the
canonicalized config path into the spawned service environment before
changing to the home directory.

Profile ports stay below 32768 so they never overlap the ephemeral
port range. An existing unhashed service registration is adopted by
the profile only when its password matches the one stored in that
profile's config, so users who set one custom OPENCODE_CONFIG_DIR keep
their running service across the upgrade instead of orphaning it.

Previously, all profiles shared ~/.local/state/opencode/service.json
and port 49374, causing secondary OPENCODE_CONFIG_DIR launches to
attach to the first profile's background server and inherit its config
and environment.

Fixes anomalyco#58141
@jskswamy
jskswamy force-pushed the isolate-service-profiles branch from ab283d9 to 73e9a3f Compare October 27, 2026 10:40
@jskswamy

Copy link
Copy Markdown
Author

Rebased onto current dev.

The only conflict was with #58331. A dev build still gets its own service. A non-default OPENCODE_CONFIG_DIR still gets its own registration and port. latest / beta / next keep sharing, including for the same profile.

@Hona the triage on #58141 called this a feature request and asked for a direction: one service per config dir (this PR), the server scoping to the client env, or a warning on mismatch. Happy to change course if one service per config dir is not what you want.

@opencode-agent opencode-agent 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 rebase onto the latest dev keeps the fix working, and the merge with the new dev-channel split and processEnv is consistent. Released channels share one hashed registration and port per config dir. A dev build with a custom config dir gets its own service-dev-<hash>.json. The spawned service's env still includes OPENCODE_CONFIG_DIR while XDG_STATE_HOME is still removed from it.

I re-ran the earlier checks from source. Two custom config dirs get separate services and stop independently, while the base attaches both to one service. Upgrading from a base-started service under one custom config dir still moves its registration, so service status and service stop reach it. test/service.test.ts (29 pass) and tsc -b pass.

Public API: same as the last review. With a non-default OPENCODE_CONFIG_DIR, the registration becomes service[-<channel>]-<hash>.json and the default port is derived from the directory (10000–32767). An existing registration is moved over when its password matches the profile's saved one.

No new concerns.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Background service is a per-user singleton: the first launch's env applies to every profile / config dir

1 participant