fix(gsd): prevent auto-mode setModel calls from persisting to settings.json#3486
fix(gsd): prevent auto-mode setModel calls from persisting to settings.json#3486deseltrus wants to merge 3 commits intogsd-build:mainfrom
Conversation
…s.json
Two setModel() calls in auto.ts omit { persist: false }, causing
transient model switches during auto-mode to silently overwrite the
user's saved default model in settings.json. Every subsequent session
then starts with the wrong model.
The affected calls are:
- stopAuto() model restore (cleanup path)
- Hook model dispatch (transient per-unit override)
All other setModel calls in the GSD extension already use persist: false.
This aligns the two remaining calls with the established pattern.
Adds a structural regression test that fails if any setModel call in
auto.ts is missing persist: false.
🔴 PR Risk Report — CRITICAL
Affected Systems
File Breakdown
|
_applyModelChange() calls setThinkingLevel() unconditionally, which
always writes the effective thinking level to settings.json. When
auto-mode dispatches use setModel({ persist: false }), the model
default is correctly protected, but the thinking level default is not.
If a transient model switch triggers thinking level clamping (e.g.
xhigh → high), the clamped level is persisted and every subsequent
session starts with the wrong thinking level.
Extract _applyThinkingLevel(level, options?) as a private method that
respects the persist option from _applyModelChange. The public
setThinkingLevel(level) API is unchanged — it delegates to
_applyThinkingLevel without options, preserving the existing persist
behavior for user-initiated changes.
The ExtensionAPI.setModel wrapper in loader.ts accepts only the model
parameter, silently dropping the options argument. This means every
pi.setModel(model, { persist: false }) call from any extension becomes
runtime.setModel(model) — the persist guard is lost and transient model
switches always overwrite the user's saved default in settings.json.
This is the actual root cause behind model and thinking level bleed
during auto-mode. The auto.ts and agent-session.ts fixes (persist:false
on setModel calls, _applyThinkingLevel persist guard) were correct but
ineffective because the options never reached the runtime.
The ExtensionCommandContext wrapper (agent-session.ts L2069) already
passes options correctly — only the loader.ts wrapper was affected.
Adversarial Review —
|
|
This PR has merge conflicts with the base branch. Please rebase or merge 🤖 Automated PR audit — 2026-04-04 |
TL;DR
What: Fix three layers of model/thinking-level persistence leaking through auto-mode.
Why: Transient model switches silently overwrite the user's saved defaults — every subsequent session starts with the wrong model and thinking level.
How: (1) Pass
optionsthrough the Extension APIsetModelwrapper, (2) addpersist: falseto auto-modesetModelcalls, (3) thread persist through thinking level updates.What
Three bugs combine to silently corrupt
settings.jsonduring auto-mode:1. Extension API wrapper drops persist option (
loader.ts) — ROOT CAUSEThe
ExtensionAPI.setModelwrapper inloader.tsaccepts onlymodel, silently dropping theoptionsargument:This means every
pi.setModel(model, { persist: false })call from any extension becomesruntime.setModel(model)— persist is always true. This single bug renders all downstream persist guards ineffective.Note: The
ExtensionCommandContextwrapper (agent-session.ts) already passes options correctly — only theloader.tswrapper was affected.2. Auto-mode setModel calls missing persist flag (
auto.ts)Two
pi.setModel()calls omit{ persist: false }:stopAuto()model restore (cleanup path)All 7 other
setModel()calls in the GSD extension already usepersist: false.3. Thinking level not guarded by persist option (
agent-session.ts)_applyModelChange()callssetThinkingLevel()unconditionally. Even when the model switch usespersist: false, the thinking level change is always written tosettings.json. If a transient switch triggers thinking level clamping (e.g.xhigh→highbecause an intermediate model lacks the capability), the clamped level is persisted.packages/pi-coding-agent/src/core/extensions/loader.tsoptionsthroughsetModelwrapperpackages/pi-coding-agent/src/core/agent-session.ts_applyThinkingLevel()that respects persist optionsrc/resources/extensions/gsd/auto.ts{ persist: false }to bothsetModel()callssrc/resources/extensions/gsd/tests/model-isolation.test.tsWhy
The combination produces this failure mode:
xhighas their defaultselectAndApplyModelpi.setModel(modelB, { persist: false })loader.tsdrops{ persist: false }→ model B is persisted tosettings.jsonsetThinkingLevel→ if model B lacksxhighsupport, thinking level is clamped tohighand persistedsettings.jsonRelated: #650 (original model config bleed report — the
auto.tsfix addresses remaining call sites, but the loader.ts bug was the actual reason previous persist guards were ineffective)How
Commit 1:
auto.ts— add{ persist: false }to remaining callsMechanical, consistent with the 7 existing calls that already use it.
Commit 2:
agent-session.ts— thread persist through thinking levelExtract
_applyThinkingLevel(level, options?)as a private method. PublicsetThinkingLevel(level)API unchanged.Commit 3:
loader.ts— pass options through wrapper (ROOT CAUSE)One-line fix that makes all downstream persist guards actually work.
Test Evidence
_applyThinkingLevelconfirmed thatpersist=undefinedreached the function even when callers passed{ persist: false }— proving the loader wrapper was dropping the argument.selectAndApplyModel→loader.js:472 setModel(model)→runtime.setModel(model)— options lost at the wrapper boundary.model-isolation.test.tsstructural test verifies allsetModelcalls inauto.tsusepersist: false. 8/8 pass.persist: falseand correctly write tosettings.json.fix