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:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user