[Bug]: Session this.model not refreshed after /model switch — auto-compaction uses stale contextWindow
Summary
When the user changes the active model via the Telegram inline /model picker, the runtime model for the next message is correctly resolved from the registry (e.g. opencode-go/deepseek-v4-flash with contextWindow: 1_000_000), but the session's in-memory agent.state.model keeps pointing to the previously selected model. As a result, checkCompaction inside AgentSession evaluates the auto-compaction threshold against the stale model's contextWindow (e.g. 200_000) and triggers compaction far too early.
/status and the webUI show the new model + the new context window (1M), so the bug is invisible from status surfaces — it only surfaces as auto-compaction firing at the wrong threshold.
Environment
OpenClaw 2026.6.5 (5181e4f)
- Default model:
opencode/deepseek-v4-flash-free (contextWindow: 200_000 per the opencode (Zen) plugin catalog)
- Switched to:
opencode-go/deepseek-v4-flash (contextWindow: 1_000_000 per extensions/opencode-go/provider-catalog.ts)
- Channel: Telegram
- Local repo HEAD:
66b91d78fe (already aligned with upstream main)
Repro
- Start a session with
opencode/deepseek-v4-flash-free (200k).
- Use the Telegram
/model inline picker → choose provider opencode-go → choose model deepseek-v4-flash. The bot replies ✅ Model changed to opencode-go/deepseek-v4-flash.
- Send a message and let the conversation grow past the previous (200k) model limit.
- Observe
/status and the webUI: model is opencode-go/deepseek-v4-flash, context window shows 1M ✅
- As tokens approach ~200k, the runtime starts auto-compaction with
reason=threshold. The actual LLM call that follows still uses the 1M model, but compaction was already triggered with a 200k budget.
Expected: compaction threshold is the active model's contextWindow (1M for opencode-go/deepseek-v4-flash).
Actual: compaction threshold is the previously active model's contextWindow (200k for opencode/deepseek-v4-flash-free).
Root cause (best evidence so far)
1. Telegram /model callback only writes the session store; it never updates the in-memory agent.state.model
extensions/telegram/src/bot-handlers.runtime.ts:2540-2620 handles the model:select callback by calling:
applyModelOverrideToSessionEntry({
entry,
selection: { provider: selection.provider, model: selection.model, isDefault: isDefaultSelection },
});
It does NOT call session.setModel(...) and does NOT set markLiveSwitchPending: true. So the running session's this.model (which is agent.state.model) keeps holding a reference to the previous model object.
2. agent.state.model is only updated via three paths
src/agents/sessions/agent-session.ts:
setModel(model) at line 1564: this.agent.state.model = model — used by slash command + extension API
cycleModel() paths at lines 1619 / 1654
refreshCurrentModelFromRegistry() at line 2247 — only triggered by registerProvider / unregisterProvider
None of these fire for the Telegram /model callback.
3. checkCompaction reads this.model?.contextWindow
src/agents/sessions/agent-session.ts:1991:
const contextWindow = this.model?.contextWindow ?? 0;
Then shouldCompact(contextTokens, contextWindow, settings) at line 2063. Since this.model is stale, the threshold is wrong.
4. Next-message model resolution is correct (so this is purely a stale-cache problem)
src/agents/embedded-agent-runner/model.ts:846 uses modelRegistry.find(provider, modelId) per attempt, and the pre-turn compaction path (compact.ts:614-619 via resolveModelAsync) does return the right model. That is why the user-facing 1M and the actual LLM request are correct — only the session-internal checkCompaction uses the stale value.
Other model params (compat, maxTokens, reasoning, cost, baseUrl, api, headers)
- Per-attempt
resolveModelAsync re-resolves all of these from the registry every run, so they are NOT stale in the actual LLM request.
- Only the
AgentSession-internal this.model snapshot is stale, and the only field read from it for compaction is contextWindow. getContextUsage() (agent-session.ts:3096) also reads the stale value, which is why the in-session percent display can disagree with /status.
What I have ruled out
Proposed fix directions (for discussion, not yet implemented)
A. Telegram handler: also call session.setModel() (and/or pass markLiveSwitchPending: true) so the in-memory this.model is refreshed.
B. AgentSession.setModel (and cycle paths) call refreshCurrentModelFromRegistry() before assigning, so a caller passing a stale snapshot is still corrected.
C. checkCompaction re-resolves the model from the registry before reading contextWindow — defense in depth, but adds a hot-path registry lookup.
(I'm happy to send a PR; just want to align on the approach.)
Suggested labels
bug
area:sessions
area:compaction
regression
telegram (or area:telegram)
[Bug]: Session
this.modelnot refreshed after/modelswitch — auto-compaction uses stalecontextWindowSummary
When the user changes the active model via the Telegram inline
/modelpicker, the runtime model for the next message is correctly resolved from the registry (e.g.opencode-go/deepseek-v4-flashwithcontextWindow: 1_000_000), but the session's in-memoryagent.state.modelkeeps pointing to the previously selected model. As a result,checkCompactioninsideAgentSessionevaluates the auto-compaction threshold against the stale model'scontextWindow(e.g. 200_000) and triggers compaction far too early./statusand the webUI show the new model + the new context window (1M), so the bug is invisible from status surfaces — it only surfaces asauto-compactionfiring at the wrong threshold.Environment
OpenClaw 2026.6.5(5181e4f)opencode/deepseek-v4-flash-free(contextWindow: 200_000per the opencode (Zen) plugin catalog)opencode-go/deepseek-v4-flash(contextWindow: 1_000_000perextensions/opencode-go/provider-catalog.ts)66b91d78fe(already aligned with upstreammain)Repro
opencode/deepseek-v4-flash-free(200k)./modelinline picker → choose provideropencode-go→ choose modeldeepseek-v4-flash. The bot replies✅ Model changed to opencode-go/deepseek-v4-flash./statusand the webUI: model isopencode-go/deepseek-v4-flash, context window shows1M✅reason=threshold. The actual LLM call that follows still uses the 1M model, but compaction was already triggered with a 200k budget.Expected: compaction threshold is the active model's
contextWindow(1M foropencode-go/deepseek-v4-flash).Actual: compaction threshold is the previously active model's
contextWindow(200k foropencode/deepseek-v4-flash-free).Root cause (best evidence so far)
1. Telegram
/modelcallback only writes the session store; it never updates the in-memoryagent.state.modelextensions/telegram/src/bot-handlers.runtime.ts:2540-2620handles themodel:selectcallback by calling:It does NOT call
session.setModel(...)and does NOT setmarkLiveSwitchPending: true. So the running session'sthis.model(which isagent.state.model) keeps holding a reference to the previous model object.2.
agent.state.modelis only updated via three pathssrc/agents/sessions/agent-session.ts:setModel(model)at line 1564:this.agent.state.model = model— used by slash command + extension APIcycleModel()paths at lines 1619 / 1654refreshCurrentModelFromRegistry()at line 2247 — only triggered byregisterProvider/unregisterProviderNone of these fire for the Telegram
/modelcallback.3.
checkCompactionreadsthis.model?.contextWindowsrc/agents/sessions/agent-session.ts:1991:Then
shouldCompact(contextTokens, contextWindow, settings)at line 2063. Sincethis.modelis stale, the threshold is wrong.4. Next-message model resolution is correct (so this is purely a stale-cache problem)
src/agents/embedded-agent-runner/model.ts:846usesmodelRegistry.find(provider, modelId)per attempt, and the pre-turn compaction path (compact.ts:614-619viaresolveModelAsync) does return the right model. That is why the user-facing1Mand the actual LLM request are correct — only the session-internalcheckCompactionuses the stale value.Other model params (compat, maxTokens, reasoning, cost, baseUrl, api, headers)
resolveModelAsyncre-resolves all of these from the registry every run, so they are NOT stale in the actual LLM request.AgentSession-internalthis.modelsnapshot is stale, and the only field read from it for compaction iscontextWindow.getContextUsage()(agent-session.ts:3096) also reads the stale value, which is why the in-session percent display can disagree with/status.What I have ruled out
agents.defaults.contextTokensis NOT set in the user'sopenclaw.json— verified by grep. So the cap path insrc/agents/context-window-guard.ts:91-94is not the cause here.findNormalizedProviderValueinpackages/model-catalog-core/src/provider-id.ts:15does NOT cross providers, so the opencodemodels.providers.opencode.models.deepseek-v4-flash-free200k entry does not bleed intoopencode-go.~/.openclaw/agents/coder/agent/plugins/opencode-go/catalog.jsonandextensions/opencode-go/provider-catalog.tsboth declarecontextWindow: 1_000_000.Proposed fix directions (for discussion, not yet implemented)
A. Telegram handler: also call
session.setModel()(and/or passmarkLiveSwitchPending: true) so the in-memorythis.modelis refreshed.B.
AgentSession.setModel(and cycle paths) callrefreshCurrentModelFromRegistry()before assigning, so a caller passing a stale snapshot is still corrected.C.
checkCompactionre-resolves the model from the registry before readingcontextWindow— defense in depth, but adds a hot-path registry lookup.(I'm happy to send a PR; just want to align on the approach.)
Suggested labels
bugarea:sessionsarea:compactionregressiontelegram(orarea:telegram)