docs(auto-agents): document model-override-needs-restart footgun (C3)

Replaces the README's optimistic "Changes to a .txt file take effect
on the next dispatched session — no OpenCode restart needed" with
the actual behaviour: BOTH paths require an OpenCode restart for a
.txt edit to change what model OpenCode generates with. The
dispatcher passes the resolved model on every POST /session
(observability / consistency check), but OpenCode itself re-resolves
agent.<name>.model from its startup-cached opencode.json on every
generation. Without a restart, the dispatcher logs say one model
ran and OpenCode actually ran another — a silent regression
invisible from the dispatcher side.

This is the same footgun the long comment in
_opencode_worker.run_session_blocking documents inline; the README
was the missing place where an operator naturally looks before
editing a model file.

Cross-links the consequence for G11 (estimator-driven adaptive
tier selection) and the in-cycle escalation plan — both silently
misbehave if a model swap lands without a restart (a Tier 1
escalation would run on the cached Tier 0 model).

Refs: docs/development/final-working-harvest-plan.md (C3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 20:20:42 -04:00
parent 094998dcfa
commit 1635229828
+52 -6
View File
@@ -15,6 +15,43 @@ Two kinds of files live here:
Two paths read from this directory; both honour the same convention.
### ⚠️ Operational footgun: editing a `.txt` file does NOT live-update generation
This is the single highest-leverage operational fact in this directory.
**Both paths below require an OpenCode server restart for an edit to a
`.txt` file to actually change what model OpenCode runs.** The dispatcher
PASSES the resolved model on every `POST /session`, but empirical
testing against OpenCode 0.x found that OpenCode re-resolves
`agent.<name>.model` from its own startup-cached `opencode.json` every
generation — the dispatcher's pass-through is recorded in the session
metadata for observability/auditability, **not** consumed by the
generation path. See the long comment in
[`tools/_opencode_worker.py`](../../tools/_opencode_worker.py)
(`run_session_blocking`, around the `POST /session` body assembly).
The implication an operator MUST internalise:
1. Edit `.opencode/models/<role>.txt` (or `tiers.yaml` + regen).
2. **Restart OpenCode** (`pkill -f 'opencode serve'` then re-launch).
3. Verify with `tools/audit_opencode_session_models.py` (or by reading
a fresh session's metadata) that the new model is in effect.
Skipping step 2 produces the worst kind of silent regression: the
dispatcher's logs say it requested the new model, the cycle archive
records the new model, but OpenCode generates with the old cached
value. There is no in-process diagnostic that surfaces the mismatch —
it is invisible from the dispatcher side.
The G11 estimator-driven adaptive tier selection
([final-working-harvest-plan.md](../../docs/development/final-working-harvest-plan.md)
§ G11) and the in-cycle escalation plan
([implementer-in-cycle-escalation-plan.md](../../docs/development/implementer-in-cycle-escalation-plan.md))
both depend on the worker actually generating with the
dispatcher-requested model. If model swaps land without a restart,
those features silently misbehave (a Tier 1 escalation will run on the
previously-cached Tier 0 model and produce the same outcome the Tier 0
attempt produced — exactly the cycle they were meant to break out of).
### Runtime (dispatcher path)
`tools/_opencode_worker.py` resolves a model for an agent by:
@@ -22,12 +59,16 @@ Two paths read from this directory; both honour the same convention.
1. Looking for `.opencode/models/<agent-name>.txt`.
2. If absent, falling back to `.opencode/models/default.txt`.
3. Parsing the single line as `providerID/modelID` and passing it as the
`model` field on `POST /session` and `POST /session/{id}/prompt_async`.
`model` field on `POST /session`.
This is the path used by `dispatch_review.py`, `dispatch_implementer.py`,
`conflict_drive.py`, and any other code that calls
`_opencode_worker.run_session_blocking`. Changes to a `.txt` file take effect
on the **next dispatched session** — no OpenCode restart needed.
`_opencode_worker.run_session_blocking`. The dispatcher includes the
resolved model on every session so the cycle archive carries the
**intended** model — but per the footgun above, OpenCode's generation
uses its startup-cached `agent.<name>.model` from `opencode.json`. Both
sides must agree, which is why an OpenCode restart after a `.txt` edit
remains required.
### Static (OpenCode config path)
@@ -54,9 +95,14 @@ python3 tools/sync_tier_models.py
# 3. Verify everything stays in sync (CI also runs this)
python3 tools/sync_tier_models.py --check # exits 0 = clean
# 4. Pick up the new model on the next dispatch — runtime path re-reads
# .txt files per cycle, no restart needed. Static path (interactive
# sessions, Task-tool subagents) requires an OpenCode restart.
# 4. RESTART OpenCode so the new model actually takes effect.
# The dispatcher records the new model on every POST /session,
# but OpenCode generates from its startup-cached opencode.json —
# skipping the restart is the silent-regression footgun called
# out at the top of this README.
pkill -f 'opencode serve' && /usr/local/bin/opencode serve & # or your launcher
# 5. Verify the new model is actually in effect on the next dispatch.
python3 tools/dispatch_implementer.py --once
```