Skip to content

Split opencode.jsonc into tracked configured default + runtime overlay (stop config drift) #4544

Closed

Description

@dezren39

Problem

.opencode/opencode.jsonc in this repo is simultaneously (a) the tracked, deliberate OpenCode configuration and (b) the file several automated processes rewrite in place in the live ~/git/code tree. That dual role is what fed the #4464 divergence: any in-tree rewrite dirties the primary, pm-git-sync cannot pull a dirty tree, the primary falls behind, and (pre-#4525) the repo-keeper snapshot-committed the dirt onto the stale base. #4525 fixed the keeper; this issue removes the drift source.

Writers found (evidence, 2026-11-02)

Automatic/runtime writers of the tracked file today:

  1. bin/sync-models.mjs — the big one. Runs daily (sync-models.timer) and opportunistically from the opencode CLI wrapper (24h stamp). Rewrites the file in place, editing only provider.*.models for five providers, from models.dev / agy discovery / the Codex cache:
    • antigravity-direct — add/update/remove entries, incl. variants + limit
    • openai-codex — add entries, fix limit
    • google, openai, cloudflare-gateway — add-only from models.dev
      Current footprint: 238 model entries (62 openai, 65 openai-codex, 54 google, 30 cloudflare-gateway, 27 antigravity-direct). The provider blocks for google and openai contain only models; the other three keep deliberate shells (npm/name/options) alongside.
  2. OpenChamber launch rewrite + opencode-config-normalize — OpenChamber migrates the v1 plugin/agent keys to v2 plugins/agents spellings in the file on launch; the path-unit normalize service rewrites them back (idempotent; net drift zero once it runs, but the file flaps on every launch). These keys are deliberate config and stay in the tracked file; the split does not change this pair's behavior.
  3. The fork server's Config.update — writes exactly one key, shell, into the global config file when a client calls the config.update RPC (fork packages/core/src/config.ts). Rare, but it is a runtime write into the tracked file.

Deliberate writers (must keep landing in the tracked file, via PRs): human/agent config edits (routes, pools, provider shells, disabled_providers).

Direct readers that must see the same effective config: the fork server (native config layering), bin/opencode-route-proxy.mjs (own JSONC parse), OpenChamber (via the server), and the pm-stack aggregator collector (AA coverage reads provider catalogs from this file).

Design

Split by ownership, using the fork's native config layering — verified empirically against the deployed build (2.0.30+6fb3bac): config documents from multiple sources are deep-merged per provider/model, and the OPENCODE_CONFIG env source ranks above the global file and is watched for hot reload.

  • Configured default (tracked): .opencode/opencode.jsonc keeps everything deliberate — provider shells, routes, pools, settings — minus the five sync-owned models maps above. google/openai blocks disappear entirely (they were models-only). Changes land only via PR.
  • Runtime changes (untracked): new file ~/.local/state/opencode/opencode-runtime.jsonc (XDG state, alongside the other relocated runtime state) holding only { provider: { <the five>: { models: {…} } } }. The server layers it over the default via OPENCODE_CONFIG; effective config is unchanged.
  • sync-models targets the runtime file (it already honors OPENCODE_CONFIG_PATH): service environment in home/common/default.nix + resolution default in the script so bare/wrapper invocations land there too once the file exists. provider-catalog-sync reads the merged effective config for diffing and skips the five sync-owned providers on --apply (sync-models owns them).
  • route proxy deep-merges the runtime file over its config at load (env OPENCODE_ROUTE_PROXY_RUNTIME_CONFIG, default the state path), cache keyed on both files. The aggregator collector merges it the same way when present (AA coverage parity).
  • fork change (developing-today/opencode): Config.update writes its patch into the explicit (OPENCODE_CONFIG) file when one is configured, else the global file as today — so the server's one runtime write also lands in the runtime file.
  • Session durability: OPENCODE_CONFIG (+ OPENCODE_CONFIG_PATH for sync-models) set in HM session variables so the server, CLI invocations, and children inherit the layering across relaunches/logins.

Migration: the five providers' current models maps move verbatim from the tracked file into the runtime file; a parse-compare proves the merged result deep-equals today's effective catalog before adoption.

Verification bar

  1. After adoption, exercising the system (server up, route calls, a sync-models run) leaves git status in ~/git/code clean — runtime writes land only in the runtime file.
  2. A deliberate config edit lands in the tracked file only, as a clean reviewable diff.
  3. Effective-config parity for server + proxy + OpenChamber against the pre-split config (same providers/models/routes; route decisions flowing; no reader errors).

Coordination

#4464's reconciliation/soak runs on the same tree: worktree/PR work proceeds independently; the live adoption (tracked-file slimming landing in the primary, server relaunch) is sequenced so it does not invalidate an in-progress soak.

Activity

  1. added 2 commits that reference this issue on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Post-adoption verification (third-attempt resume agent, 2026-11-02 ~14:11–14:15 CST)

    The implementing agent's completion report was lost to a transport timeout; the work itself was complete (this PR's merge e2208450 is the laptop primary's HEAD, fork companion developing-today-forks/opencode#1429 merged 4a2f3e11). The verification bar from the issue body was re-run independently against the live system:

    1. Runtime writes land only in the runtime file. Ran bin/sync-models.mjs live: it pruned 23 stale models and wrote only ~/.local/state/opencode/opencode-runtime.jsonc (its own log line + backup alongside it in the state dir). The tracked file's sha256 was byte-identical before/after (503c76413946b268…), and git status --porcelain -- .opencode/ was 0 lines before and after.
    2. Deliberate edit = one clean diff. In a disposable worktree at HEAD, a one-line deliberate change (autoupdate) produced exactly a 1-file, 1-insertion/1-deletion diff in the tracked file. Worktree removed; primary untouched.
    3. Effective-config parity.
      • File level: the overlay holds exactly the five sync-owned providers' models maps — 238 entries (antigravity-direct 27, openai-codex 65, google 54, openai 62, cloudflare-gateway 30) — and the tracked file holds 0 models for those five (the models-only google/openai blocks are gone); all 24 model_routes remain in the tracked file.
      • Server (relaunched 14:05:10 with OPENCODE_CONFIG=~/.local/state/opencode/opencode-runtime.jsonc in its environment): /api/config lists both documents layered (tracked global + the runtime file); /api/provider shows overlay-only antigravity-direct with its full 27-model catalog in the effective config.
      • Route proxy (restarted 14:04:45 in the adoption window, running the deep-merge code): /v1/models lists the routes, and a live summit completion through :18750 returned a real response with usage — summit's antigravity-direct leaf catalog now comes from the overlay. (A workhorse call in the same window surfaced an OpenCode Go monthly-limit 429 from a leaf — a quota state, not a config error; the route resolved and the provider answered.)
      • OpenChamber attaches to this same server, so its effective config is the server's, shown above.

    Sequencing note: a fleet laptop switch was in progress throughout this verification; nothing here relaunched the server or touched the primary tree (the only writes were sync-models → state dir, and a disposable worktree).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions