anomalyco / anomalyco/opencode
TUI: session model hydration is lost under OPENCODE_FAST_BOOT
@kommander is already working on this.
Since Jul 26, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
With OPENCODE_FAST_BOOT=1, opening a session whose last user message used a non-default model leaves the composer on the default model instead. The session's own selection is silently dropped.
This matters beyond the wrong label in the footer: Anthropic prompt caching keys on the model, so the next turn misses the cache prefix and re-uploads the whole context.
Cause
packages/tui/src/component/prompt/index.tsx:310-332 hydrates agent/model from the session's last user message, guarded by a one-shot latch:
if (sessionID !== syncedSessionID) {
if (!sessionID || !msg) return
syncedSessionID = sessionID // latched here
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
if (msg.agent && isPrimaryAgent) { // bail 1
if (!args.agent) local.agent.set(msg.agent)
if (msg.model) {
local.model.set(msg.model) // bail 2
local.model.variant.set(msg.model.variant)
}
}
}
The latch is assigned before either setter runs, and both setters no-op on data that has not arrived yet:
local.agent.list()is empty until the agent catalog loads, soisPrimaryAgentis false and the whole block is skipped.local.model.set()returns early whenisModelValid()fails, and that checkssync.data.provider(packages/tui/src/context/local.tsx:64-67), still empty while providers load.
Either way the latch is already set, so the effect never re-enters for that session. Selection falls through to fallbackModel — CLI arg, then config model, then recent[0], then the provider default. Since recent[0] is persisted in model.json, you usually land on whatever you picked last rather than an obviously wrong model, which makes it look intermittent.
Normal boot is unaffected. SyncProvider gates the app behind init.ready (packages/tui/src/context/helper.tsx:15), and ready is false while loading unless fast-boot is on (packages/tui/src/context/sync.tsx:558). Prompt does not mount until the catalogs are in. Fast-boot removes that gate, which is what exposes the race.
Reproducing
packages/tui/test/cli/tui/model-hydration-race.test.tsx in the linked PR mounts the real contexts with skipInitialLoading: true, resolves /api/agent after the session's messages are already available, and asserts the composer ends up on the session's model.
Against current dev:
Expected: beta/repro
Received: alpha/model-a
At the moment the effect runs: the message is present, local.agent.list() is [], and sync.data.provider is [] — bail 1.
The same test with skipInitialLoading: false passes on unfixed source, which is how the scope was established rather than assumed.
Fix
Move the latch after the hydration and let the effect retry while the catalogs are still loading. Both reads stay tracked, so it re-runs when the data lands and settles once applied. Once loaded, behaviour is unchanged — one-shot per session, and an unsent local model choice is never overwritten.
PR: #38941
Related
Several open issues describe a session reverting to the default model, but through different paths — #38165 (resume picks the last command's model), #38770 and #28735 (a background subagent notification reverts the selection). This one is specifically the fast-boot mount race; it does not explain those.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.