@@ -158,7 +158,7 @@ func (s *ConfigStore) SetConfigFields(scope Scope, kv map[string]any) error {
158158 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
159159 return fmt.Errorf("failed to create config directory %q: %w", path, err)
160160 }
161− if err := os.WriteFile(path, []byte(newValue), 0o600); err != nil {
161+ if err := atomicWriteFile(path, []byte(newValue), 0o600); err != nil {
162162 return fmt.Errorf("failed to write config file: %w", err)
163163 }
164164
@@ -193,7 +193,7 @@ func (s *ConfigStore) RemoveConfigField(scope Scope, key string) error {
193193 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
194194 return fmt.Errorf("failed to create config directory %q: %w", path, err)
195195 }
196− if err := os.WriteFile(path, []byte(newValue), 0o600); err != nil {
196+ if err := atomicWriteFile(path, []byte(newValue), 0o600); err != nil {
197197 return fmt.Errorf("failed to write config file: %w", err)
198198 }
199199
@@ -302,7 +302,10 @@ func (s *ConfigStore) SetProviderAPIKey(scope Scope, providerID string, apiKey a
302302// RefreshOAuthToken refreshes the OAuth token for the given provider.
303303// Before making an external refresh request, it checks the config file on
304304// disk to see if another Crush session has already refreshed the token. If
305−// a newer token is found, it is used instead of refreshing.
305+// a newer, unexpired token is found, it is used instead of refreshing. If
306+// the exchange fails (e.g. because another session already rotated the
307+// refresh token), the disk is re-checked to recover the other session's
308+// token.
306309func (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, providerID string) error {
307310 providerConfig, exists := s.config.Providers.Get(providerID)
308311 if !exists {
@@ -318,15 +321,9 @@ func (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, provid
318321 newToken, err := s.loadTokenFromDisk(scope, providerID)
319322 if err != nil {
320323 slog.Warn("Failed to read token from config file, proceeding with refresh", "provider", providerID, "error", err)
321− } else if newToken != nil && newToken.AccessToken != providerConfig.OAuthToken.AccessToken {
324+ } else if newToken != nil && !newToken.IsExpired() && newToken.AccessToken != providerConfig.OAuthToken.AccessToken {
322325 slog.Info("Using token refreshed by another session", "provider", providerID)
323− providerConfig.OAuthToken = newToken
324− providerConfig.APIKey = newToken.AccessToken
325− if providerID == string(catwalk.InferenceProviderCopilot) {
326− providerConfig.SetupGitHubCopilot()
327− }
328− s.config.Providers.Set(providerID, providerConfig)
329− return nil
326+ return s.applyToken(providerConfig, newToken, providerID)
330327 }
331328
332329 var refreshedToken *oauth.Token
@@ -340,6 +337,16 @@ func (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, provid
340337 return fmt.Errorf("OAuth refresh not supported for provider %s", providerID)
341338 }
342339 if refreshErr != nil {
340+ // The exchange may have failed because another session already
341+ // rotated the refresh token. Re-read the config file and use the
342+ // other session's token if available.
343+ if diskToken, diskErr := s.loadTokenFromDisk(scope, providerID); diskErr == nil &&
344+ diskToken != nil &&
345+ !diskToken.IsExpired() &&
346+ diskToken.AccessToken != providerConfig.OAuthToken.AccessToken {
347+ slog.Info("Using token refreshed by another session after exchange failure", "provider", providerID)
348+ return s.applyToken(providerConfig, diskToken, providerID)
349+ }
343350 return fmt.Errorf("failed to refresh OAuth token for provider %s: %w", providerID, refreshErr)
344351 }
345352
@@ -364,6 +371,17 @@ func (s *ConfigStore) RefreshOAuthToken(ctx context.Context, scope Scope, provid
364371 return nil
365372}
366373
374+// applyToken updates the in-memory provider config with the given token.
375+func (s *ConfigStore) applyToken(providerConfig ProviderConfig, token *oauth.Token, providerID string) error {
376+ providerConfig.OAuthToken = token
377+ providerConfig.APIKey = token.AccessToken
378+ if providerID == string(catwalk.InferenceProviderCopilot) {
379+ providerConfig.SetupGitHubCopilot()
380+ }
381+ s.config.Providers.Set(providerID, providerConfig)
382+ return nil
383+}
384+
367385// loadTokenFromDisk reads the OAuth token for the given provider from the
368386// config file on disk. Returns nil if the token is not found or matches the
369387// current in-memory token.