Skip to content

[Bug]: Session this.model not refreshed after /model switch — auto-compaction uses stale contextWindow #92379

Description

@samson910022

[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

  1. Start a session with opencode/deepseek-v4-flash-free (200k).
  2. 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.
  3. Send a message and let the conversation grow past the previous (200k) model limit.
  4. Observe /status and the webUI: model is opencode-go/deepseek-v4-flash, context window shows 1M
  5. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions