@@ -221,6 +221,16 @@ func (s *ConfigStore) SetConfigField(scope Scope, key string, value any) error {
221221// The write is protected by an in-process mutex and a cross-process flock
222222// to prevent races between concurrent writers in different processes.
223223func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
224+ return s.setConfigFields(scope, kv, true)
225+}
226+
227+// setConfigFields performs the write and, when reload is true, refreshes
228+// in-memory state from disk. Callers that have already updated the relevant
229+// in-memory state themselves can pass reload=false to skip the expensive
230+// full reparse (which rebuilds the provider catalog and agents). The disk
231+// is the source of truth for other processes either way; skipping reload
232+// only avoids re-deriving state this process already holds.
233+func (s *ConfigStore) setConfigFields(scope Scope, kv map[string]any, reload bool) error {
224234 // Sort keys for deterministic output regardless of map iteration
225235 // order. This also ensures consistent results when callers pass
226236 // overlapping JSONPath keys (e.g. "a" and "a.b").
@@ -245,6 +255,16 @@ func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
245255 return err
246256 }
247257
258+ if !reload {
259+ // The caller has already updated in-memory state; just refresh
260+ // the staleness snapshot so the file watcher does not treat our
261+ // own write as an external change.
262+ if path, perr := s.configPath(scope); perr == nil {
263+ s.captureStalenessSnapshot(append(slices.Clone(s.loadedPaths), path))
264+ }
265+ return nil
266+ }
267+
248268 // Auto-reload to keep in-memory state fresh after config edits.
249269 // We use context.Background() since this is an internal operation that
250270 // shouldn't be cancelled by user context.
@@ -281,14 +301,30 @@ func (s *ConfigStore) RemoveConfigField(scope Scope, key string) error {
281301}
282302
283303// UpdatePreferredModel updates the preferred model for the given type and
284−// persists it to the config file at the given scope.
304+// persists it to the config file at the given scope. The selected model and
305+// the recent-models list are written together in a single config write.
306+//
307+// The in-memory config is updated directly here, so the write skips the
308+// full disk reparse/reload. The reload would otherwise rebuild the entire
309+// provider catalog and agents on every model switch, which dominates the
310+// latency of selecting a model. Agents are refreshed separately by the
311+// caller (see UpdateAgentModel).
285312func (s *ConfigStore) UpdatePreferredModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
286313 s.config.Models[modelType] = model
287− if err := s.SetConfigField(scope, fmt.Sprintf("models.%s", modelType), model); err != nil {
288− return fmt.Errorf("failed to update preferred model: %w", err)
314+
315+ fields := map[string]any{
316+ fmt.Sprintf("models.%s", modelType): model,
289317 }
290− if err := s.recordRecentModel(scope, modelType, model); err != nil {
291− return err
318+
319+ // Compute the updated recent-models list in memory. When it changes,
320+ // fold the write into the same batch so we avoid a second write.
321+ if updated, changed := s.nextRecentModels(modelType, model); changed {
322+ s.config.RecentModels[modelType] = updated
323+ fields[fmt.Sprintf("recent_models.%s", modelType)] = updated
324+ }
325+
326+ if err := s.setConfigFields(scope, fields, false); err != nil {
327+ return fmt.Errorf("failed to update preferred model: %w", err)
292328 }
293329 return nil
294330}
@@ -504,10 +540,15 @@ func (s *ConfigStore) loadTokenFromDisk(scope Scope, providerID string) (*oauth.
504540 return &token, nil
505541}
506542
507−// recordRecentModel records a model in the recent models list.
508−func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
543+// nextRecentModels computes the recent-models list for the given type
544+// after recording the supplied model at the front, without persisting
545+// anything. It returns the new slice and whether it differs from the
546+// current list. It also lazily initializes the RecentModels map. Callers
547+// that want to persist the change can fold the result into a batched
548+// config write; recordRecentModel wraps it for the standalone case.
549+func (s *ConfigStore) nextRecentModels(modelType SelectedModelType, model SelectedModel) ([]SelectedModel, bool) {
509550 if model.Provider == "" || model.Model == "" {
510− return nil
551+ return nil, false
511552 }
512553
513554 if s.config.RecentModels == nil {
@@ -534,6 +575,19 @@ func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType
534575 }
535576
536577 if slices.EqualFunc(current, updated, eq) {
578+ return current, false
579+ }
580+
581+ return updated, true
582+}
583+
584+// recordRecentModel records a model in the recent models list and persists
585+// the change. It is retained for callers that update recent models in
586+// isolation; UpdatePreferredModel batches the same change into a single
587+// config write instead.
588+func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
589+ updated, changed := s.nextRecentModels(modelType, model)
590+ if !changed {
537591 return nil
538592 }
539593