Skip to content

[AI] fix(model): /model <default> writes override when session runs non-default model#96368

Closed
xydt-tanshanshan wants to merge 1 commit into
openclaw:mainfrom
xydt-tanshanshan:fix/model-default-switch-96269
Closed

[AI] fix(model): /model <default> writes override when session runs non-default model#96368
xydt-tanshanshan wants to merge 1 commit into
openclaw:mainfrom
xydt-tanshanshan:fix/model-default-switch-96269

Conversation

@xydt-tanshanshan

Copy link
Copy Markdown
Contributor

Summary 📌 required

/model <default> silently fails when the session is currently running a non-default model. Strip isDefault from model selections in directive-handling paths so user-requested model switches always write an explicit override.

  • Problem: When a session runs a non-default model (via prior /model, fallback, or steering) and the user types /model <default>, the selection gets isDefault=true. This clears overrides in applyModelOverrideToSessionEntry instead of writing them, leaving the session pinned to the stale runtime model.
  • Solution: Strip the isDefault flag from ModelDirectiveSelection before passing it to applyModelOverrideToSessionEntry in the two directive-handling call sites. Keep isDefault on the selection object for display/sorting purposes.
  • What changed: src/auto-reply/reply/directive-handling.impl.ts (+8/-1), src/auto-reply/reply/directive-handling.persist.ts (+4/-1), src/sessions/model-overrides.test.ts (+32 regression test)
  • What did NOT change: applyModelOverrideToSessionEntry itself, repair/cleanup paths (agent-command, model-selection reset, repairProviderWrappedModelOverride), isDefault display semantics in model picker/suggestions

Change Type (select all) 📌 required

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all) 📌 required

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR 📌 required

Motivation 📌 required

When a session is running a non-default model (set via /model, fallback, or steering) and the user explicitly issues /model <default-model>, the session should switch to the configured default model. Instead, the switch silently fails because isDefault=true causes applyModelOverrideToSessionEntry to clear overrides rather than writing the default as an explicit override. The session remains on the old model with no error or feedback.

This is a regression in behaviour — prior versions handled this correctly because the model-override path did not distinguish between "explicit user selection" and "system reset to default."

Real behavior proof (required for external PRs) 📌 required

  • Behavior addressed: /model <default> switching to the configured default model when the session is currently running a different model
  • Real environment tested: Linux 4.19, Node 22.19, branch fix/model-default-switch-96269
  • Exact steps or command run after this patch:
node scripts/run-vitest.mjs src/sessions/model-overrides.test.ts --run
node scripts/run-vitest.mjs src/auto-reply/reply/directive-handling.model.test.ts src/auto-reply/reply/model-selection.test.ts --run
  • Evidence after fix:
$ node scripts/run-vitest.mjs src/sessions/model-overrides.test.ts --run
[test] starting test/vitest/vitest.unit-fast.config.ts
 RUN  v4.1.8
 Test Files  1 passed (1)
      Tests  10 passed (10)
   Start at  17:50:42
   Duration  488ms

$ node scripts/run-vitest.mjs src/auto-reply/reply/directive-handling.model.test.ts src/auto-reply/reply/model-selection.test.ts --run
[test] starting test/vitest/vitest.auto-reply.config.ts
 RUN  v4.1.8
 Test Files  2 passed (2)
      Tests  132 passed (132)
   Start at  17:50:58
   Duration  18.37s
  • Observed result after fix:

    1. New regression test "writes default model override when selected without isDefault flag (Bug: /model silently fails when switching to default model while running a different model #96269)" passes — verifies that when the configured default model is selected without isDefault, the override is written even when the session currently has a non-default override
    2. All 9 existing tests continue to pass — no regression in isDefault=true clearing behaviour for repair/cleanup paths
    3. All 132 directive-handling and model-selection tests pass — no regression in the /model command resolution pipeline
  • What was not tested:

    • End-to-end webchat /model roundtrip with a live gateway (requires real provider credentials)
    • The isDefault display behaviour in the model picker UI (the flag is preserved on the selection object for display)

Root Cause (if applicable) ✅ optional

In resolveModelSelectionFromDirective (directive-handling.model-selection.ts), when the user types /model <default-model>, the resolved ModelDirectiveSelection carries isDefault: true because the selected model matches the configured agents.defaults.model.primary. This flag flows to applyModelOverrideToSessionEntry, where the isDefault branch unconditionally clears provider/model overrides — even when the session is currently running a different model. Clearing overrides leaves the session pinned to the stale runtime model without triggering a switch.

The fix strips isDefault before the selection reaches applyModelOverrideToSessionEntry, so user-initiated model switches always write an explicit override. The isDefault flag is preserved on the modelSelection object for display purposes (model picker sorting, switch event messages).

Regression Test Plan (if applicable) ✅ optional

Added model-overrides.test.ts: "writes default model override when selected without isDefault flag (#96269)" — verifies that a selection matching the configured default without the isDefault flag writes providerOverride and modelOverride even when the session entry currently holds a different override.

User-visible / Behavior Changes 📌 required

/model <default> now correctly switches the session to the configured default model when the session is currently running a non-default model. Previously the switch silently failed — /status would still show the old model with no error feedback.

Diagram (if applicable) ✅ optional

Before (broken):
  Session runs: tencent/glm-5.1 (override)
  User types:   /model minimax/MiniMax-M3  (configured default)
  isDefault=true → applyModelOverrideToSessionEntry clears overrides
  Session stays: tencent/glm-5.1  ← BUG: no switch, no error

After (fixed):
  Session runs: tencent/glm-5.1 (override)
  User types:   /model minimax/MiniMax-M3  (configured default)
  isDefault stripped → applyModelOverrideToSessionEntry writes override
  Session now:  minimax/MiniMax-M3  ← CORRECT

Security Impact (required) 📌 required

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification ✅ optional

Environment

  • OS: Linux 4.19.112-2.el8
  • Runtime/container: Node 22.19
  • Model/provider: N/A (unit-test verified)
  • Integration/channel: N/A (directive-handling path)

Steps

  1. Configure agents.defaults.model.primary: "minimax/MiniMax-M3"
  2. Switch session to a different model: /model tencent-token-plan/glm-5.1
  3. Switch back to default: /model minimax/MiniMax-M3
  4. Check /status — should show minimax/MiniMax-M3

Expected

Session switches to minimax/MiniMax-M3.

Actual (before fix)

Session stays on tencent-token-plan/glm-5.1. No error or feedback.

Human Verification (required) 📌 required

  • Verified scenarios: Unit test covers the exact scenario from Bug: /model silently fails when switching to default model while running a different model #96269 (session with non-default override, user selects default model without isDefault flag)
  • Edge cases checked: existing isDefault=true clearing behaviour preserved for repair/cleanup paths; test for isDefault=true with already-matching override still clears correctly
  • What you did NOT verify: Live webchat end-to-end /model roundtrip

Compatibility / Migration 📌 required

  • Backward compatible? Yes — no API or config changes
  • Config/env changes? No
  • Migration needed? No

Best-fix Verdict 📌 required (from Review Contract)

  • Best fix: Yes — stripping isDefault at the directive-handling boundary is the right fix because:
    1. It separates the display concern (isDefault for model picker sorting) from the behavioural concern (explicit model switch)
    2. It does not modify applyModelOverrideToSessionEntry, preserving existing semantics for repair/cleanup paths
    3. The fix boundary matches the intent boundary: user-requested model selections should always write explicit overrides
  • Refactor needed: No
  • Alternative considered:
    • Fix applyModelOverrideToSessionEntry isDefault branch: Adding a hasNonDefaultOverride check inside the callee would also work, but it would change behaviour for all callers including repair/cleanup paths where clearing is the correct intent. The callee lacks enough context to distinguish user-initiated switches from system-initiated cleanup.
    • Remove isDefault from resolveModelDirectiveSelection: Would affect model picker display/sorting since isDefault is consumed downstream for UI purposes.

AI Assistance 🤖 (required for AI-assisted PRs)

  • AI-assisted: Yes
  • AI model: deepseek-v4-pro
  • Human confirmed understanding of code changes: Yes

Risks and Mitigations 📌 required

  • Highest-risk area: Existing sessions that rely on isDefault=true clearing behaviour from the directive-handling path. Previously, if a user typed /model <default> while already on the default model, overrides would be cleared (no-op since none existed). Now an explicit override is written. This is a semantic change but the practical effect is identical — the session runs the default model either way.
  • Mitigation: The override values written match the configured default model, so session behaviour is unchanged. If the config default later changes, this session retains its explicit override (which is the correct behaviour for a user who explicitly selected a model).
  • Compatibility impact: None. No config, API, or storage format changes.

Related to #96269

…on-default model

Strip isDefault from model selections before applying overrides in the
directive-handling paths.  When a user explicitly switches to the
configured default model via /model, the selection must write an
override so the session actually transitions — clearing overrides left
the session pinned to the stale runtime model.

Related to openclaw#96269
@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: this branch changes directive-only model persistence, but the linked webchat bug runs through sessions.patch, and a cleaner open PR already targets the shared helper while preserving default-reset storage semantics.

Root-cause cluster
Relationship: superseded
Canonical: #96318
Summary: This PR is superseded by the open proof-positive helper-level PR for the same default-model live-switch root cause.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Use #96318 or an equivalent helper-level fix that preserves default-reset storage while recording live switch intent, then verify the gateway path before closing the linked issue.

So I’m closing this here and keeping the remaining discussion on #96318.

Review details

Best possible solution:

Use #96318 or an equivalent helper-level fix that preserves default-reset storage while recording live switch intent, then verify the gateway path before closing the linked issue.

Do we have a high-confidence way to reproduce the issue?

Yes at source level: webchat /model calls sessions.patch, which passes isDefault into the shared helper, and live switching depends on liveModelSwitchPending. I did not run a live webchat/provider reproduction in this read-only review.

Is this the best way to solve the issue?

No. The best fix is the shared helper or gateway path used by sessions.patch, preserving default-reset storage while setting switch intent; this PR instead changes directive storage semantics and is superseded by the cleaner open candidate.

Security review:

Security review cleared: The diff only changes TypeScript session/directive code and a test; it does not touch dependencies, CI, secrets, permissions, network calls, or package execution surfaces.

AGENTS.md: found and applied where relevant.

What I checked:

  • Repository policy read: Read the full root AGENTS.md and applied its ClawSweeper PR guidance to review beyond the touched diff and compare caller/callee paths. (AGENTS.md:1, e9720c27fa69)
  • Current webchat entry point: The reported webchat /model command sends a sessions.patch request with the requested model, so the central linked issue is owned by the gateway patch path rather than only auto-reply directive persistence. (ui/src/ui/chat/slash-command-executor.ts:214, e9720c27fa69)
  • Current gateway model patch path: sessions.patch resolves whether the requested model is the configured default and passes isDefault plus markLiveSwitchPending: true to applyModelOverrideToSessionEntry. (src/gateway/sessions-patch.ts:569, e9720c27fa69)
  • Current helper semantics: applyModelOverrideToSessionEntry clears provider/model overrides when selection.isDefault and only writes liveModelSwitchPending under the existing updated/selectionUpdated gate. (src/sessions/model-overrides.ts:42, e9720c27fa69)
  • Documented default-reset contract: The docs say existing session selections can be cleared with /model default so the session inherits the configured primary; this PR would instead pin the then-current default as a user override in directive paths. Public docs: docs/concepts/models.md. (docs/concepts/models.md:63, e9720c27fa69)
  • Current PR diff: The branch strips isDefault in handleDirectiveOnly and persistInlineDirectives, making default selections write explicit overrides in those paths. (src/auto-reply/reply/directive-handling.impl.ts:458, 422ff8d7d0c7)

Likely related people:

  • vincentkoc: Current blame on the shared model override helper, sessions.patch model handling, and live-switch files points to the recent current-main commit that carried this surface forward. (role: current-main implementation carrier; confidence: medium; commits: 2ad2e4f2dc6c; files: src/sessions/model-overrides.ts, src/gateway/sessions-patch.ts, src/agents/live-model-switch.ts)
  • jalehman: Recent history in the linked issue review and local commit metadata tie this contributor to session accessor and live model read refactors adjacent to the affected state path. (role: recent area contributor; confidence: medium; commits: f8ed4de460f7, 96cee6cb6442, 6f2869c296ff; files: src/sessions/model-overrides.ts, src/gateway/sessions-patch.ts, src/agents/live-model-switch.ts)
  • kiranvk2011: Git history shows this contributor added the explicit liveModelSwitchPending behavior that the remaining fix path depends on. (role: live model switch contributor; confidence: medium; commits: 251e086eacbd; files: src/agents/live-model-switch.ts)
  • steipete: Earlier history ties this contributor to applying live model switches during active retries, the downstream consumer of the pending flag. (role: feature-history contributor; confidence: medium; commits: 7dd196ed740a; files: src/agents/live-model-switch.ts)

Codex review notes: model internal, reasoning high; reviewed against e9720c27fa69.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels Jun 24, 2026
@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant