Repository navigation
Description
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:
bin/sync-models.mjs— the big one. Runs daily (sync-models.timer) and opportunistically from theopencodeCLI wrapper (24h stamp). Rewrites the file in place, editing onlyprovider.*.modelsfor five providers, from models.dev / agy discovery / the Codex cache:antigravity-direct— add/update/remove entries, incl.variants+limitopenai-codex— add entries, fixlimitgoogle,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 forgoogleandopenaicontain onlymodels; the other three keep deliberate shells (npm/name/options) alongside.
- OpenChamber launch rewrite +
opencode-config-normalize— OpenChamber migrates the v1plugin/agentkeys to v2plugins/agentsspellings 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. - The fork server's
Config.update— writes exactly one key,shell, into the global config file when a client calls the config.update RPC (forkpackages/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.jsonckeeps everything deliberate — provider shells, routes, pools, settings — minus the five sync-ownedmodelsmaps above.google/openaiblocks 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 viaOPENCODE_CONFIG; effective config is unchanged. - sync-models targets the runtime file (it already honors
OPENCODE_CONFIG_PATH): service environment inhome/common/default.nix+ resolution default in the script so bare/wrapper invocations land there too once the file exists.provider-catalog-syncreads 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.updatewrites 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_PATHfor 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
- After adoption, exercising the system (server up, route calls, a sync-models run) leaves
git statusin ~/git/code clean — runtime writes land only in the runtime file. - A deliberate config edit lands in the tracked file only, as a clean reviewable diff.
- 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
feat(opencode): split opencode.jsonc into tracked default + runtime o…
Merge pull request #4554 from developing-today/chore/opencode-config-…
dezren39 commentedon Nov 2, 2026
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
e2208450is the laptop primary's HEAD, fork companion developing-today-forks/opencode#1429 merged4a2f3e11). The verification bar from the issue body was re-run independently against the live system:bin/sync-models.mjslive: 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…), andgit status --porcelain -- .opencode/was 0 lines before and after.autoupdate) produced exactly a 1-file, 1-insertion/1-deletion diff in the tracked file. Worktree removed; primary untouched.modelsmaps — 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-onlygoogle/openaiblocks are gone); all 24model_routesremain in the tracked file.OPENCODE_CONFIG=~/.local/state/opencode/opencode-runtime.jsoncin its environment):/api/configlists both documents layered (tracked global + the runtime file);/api/providershows overlay-onlyantigravity-directwith its full 27-model catalog in the effective config./v1/modelslists the routes, and a livesummitcompletion through :18750 returned a real response with usage — summit's antigravity-direct leaf catalog now comes from the overlay. (Aworkhorsecall 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.)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).
fix(opencode-config): normalise to canonical v2 plugins/agents keys