Skip to content

service: make subprocess PATH deterministic across reconnect election #38897

Closed

Description

@opencode-agent

Summary

The V2 managed background service inherits the environment of whichever reconnecting client wins service election. When several clients reconnect after a restart, crash, or version replacement, they race to spawn detached contenders with potentially different PATH values.

The winning environment becomes the process-global environment used by shell calls. As a result, the same command can work before a service replacement and fail afterward with command not found (observed with curl).

Reproduction

A bounded harness exercised the current Service.ensure path with two simultaneous reconnecting clients:

  1. Client A had a PATH containing a fixture curl executable.
  2. Client B had a PATH without curl.
  3. Both called Service.ensure concurrently after the registration disappeared.
  4. The elected service ran /bin/sh -c curl using its inherited environment.
  5. The election was repeated 20 times.

Result:

{"found":10,"missing":10}

The command outcome depended only on which client's contender won election.

Cause

  • packages/client/src/effect/service.ts spawns each contender with inherited environment via spawn(command, args, { detached: true, stdio: "ignore" }).
  • A restart or service loss causes every attached client reconnect loop to call Service.ensure, so contenders may originate from different terminal, IDE, or launcher environments.
  • packages/core/src/shell.ts builds every shell environment from the elected server's process.env.
  • Repository search found no production path that deletes, replaces, or mutates PATH; this is environment replacement across server processes, not mutation within one server.

This is adjacent to the reconnect herd documented in #36285, but the user-visible failure here is nondeterministic process environment rather than contender resource overhead.

Expected behavior

Shell command resolution should remain deterministic across managed-service replacement and should not depend on which attached client wins a reconnect race.

Design options

  1. Canonical service environment: derive the service environment from the user's login shell on startup, independent of the spawning contender. Desktop already has a bounded login-shell environment probe that may be reusable after extraction.
  2. Persist a canonical local environment: choose and retain one non-secret environment contract across service replacements. This needs careful treatment of changing toolchains and secret values.
  3. Explicit client/session environment: pass the relevant execution environment through an owned client, session, or location boundary rather than relying on server-global process.env. This is more precise but requires conflict semantics for multiple clients.

Questions

  • Which boundary should own local command environment in the shared-service architecture?
  • Should activated environments such as mise, direnv, Nix, or virtualenv follow the client/session, or should the service always use a canonical login-shell environment?
  • What environment data is safe to persist or send over the local API?
  • Should shell, PTY, MCP, LSP, formatter, and other subprocess producers share one environment policy?

Source discussion: https://slack.com/archives/C0BE69AHCQP/p1785009702750149

Requested by: @rekram1-node (Aiden Cline via Slack)

Activity

  1. added
    bugSomething isn't working
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Jul 25, 2026
  2. ogulcancelik commented on Aug 16, 2026

    @ogulcancelik

    @rekram1-node i’m hitting the same root cause from herdr. herdr gives each pane its identity through client-local variables such as HERDR_PANE_ID and HERDR_SOCKET_PATH. with the shared service, a turn submitted from client b still runs with the environment of whichever client started the server.

    i reproduced this on opencode2 v0.0.0-next-17444:

    server environment:  HERDR_PANE_ID=server-pane
    client environment:  HERDR_PANE_ID=client-pane
    shell tool received: HERDR_PANE_ID=server-pane
    

    an opencode setting could list which client environment variables should be forwarded. the TUI would read those values and attach them to each turn, with tool calls and subagents inheriting that turn’s environment. storing static values in server config would not work because each attached client can belong to a different herdr pane.

    --standalone works around this, but shared-server clients still cannot identify their originating pane.

  3. neriousy commented on Oct 21, 2026

    @neriousy
    Member

    Done in #42957: clients now send their terminal environment per session and the shell tool uses it (packages/core/src/shell.ts), so PATH no longer depends on which client wins service election. #43269 and #50907 also added a configurable, persisted service environment.

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

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions