@@ -221,6 +221,16 @@ func (s *ConfigStore) SetConfigField(scope Scope, key string, value any) error {
221// The write is protected by an in-process mutex and a cross-process flock
221// The write is protected by an in-process mutex and a cross-process flock
222// to prevent races between concurrent writers in different processes.
222// to prevent races between concurrent writers in different processes.
223func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
223func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
224+ return s.setConfigFields(scope, kv, true)
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 {
224 // Sort keys for deterministic output regardless of map iteration
234 // Sort keys for deterministic output regardless of map iteration
225 // order. This also ensures consistent results when callers pass
235 // order. This also ensures consistent results when callers pass
226 // overlapping JSONPath keys (e.g. "a" and "a.b").
236 // overlapping JSONPath keys (e.g. "a" and "a.b").
@@ -245,6 +255,16 @@ func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
245 return err
255 return err
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))
248 // Auto-reload to keep in-memory state fresh after config edits.
268 // Auto-reload to keep in-memory state fresh after config edits.
249 // We use context.Background() since this is an internal operation that
269 // We use context.Background() since this is an internal operation that
250 // shouldn't be cancelled by user context.
270 // shouldn't be cancelled by user context.
@@ -281,14 +301,30 @@ func (s *ConfigStore) RemoveConfigField(scope Scope, key string) error {
283// UpdatePreferredModel updates the preferred model for the given type and
303// 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.
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).
285func (s *ConfigStore) UpdatePreferredModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
312func (s *ConfigStore) UpdatePreferredModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
286 s.config.Models[modelType] = model
313 s.config.Models[modelType] = model
315+ fields := map[string]any{
287− if err := s.SetConfigField(scope, fmt.Sprintf("models.%s", modelType), model); err != nil {
316+ fmt.Sprintf("models.%s", modelType): model,
288− return fmt.Errorf("failed to update preferred model: %w", err)
290− if err := s.recordRecentModel(scope, modelType, model); err != nil {
318+
291− return err
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
326+ if err := s.setConfigFields(scope, fields, false); err != nil {
327+ return fmt.Errorf("failed to update preferred model: %w", err)
293 return nil
329 return nil
@@ -504,10 +540,15 @@ func (s *ConfigStore) loadTokenFromDisk(scope Scope, providerID string) (*oauth.
504 return &token, nil
540 return &token, nil
507−// recordRecentModel records a model in the recent models list.
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.
508−func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType, model SelectedModel) error {
549+func (s *ConfigStore) nextRecentModels(modelType SelectedModelType, model SelectedModel) ([]SelectedModel, bool) {
509 if model.Provider == "" || model.Model == "" {
550 if model.Provider == "" || model.Model == "" {
510− return nil
551+ return nil, false
513 if s.config.RecentModels == nil {
554 if s.config.RecentModels == nil {
@@ -534,6 +575,19 @@ func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType
536 if slices.EqualFunc(current, updated, eq) {
577 if slices.EqualFunc(current, updated, eq) {
578+ return current, false
581+ return updated, true
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)
537 return nil
591 return nil