From 39597267ae0158a092424103b29126321568ec57 Mon Sep 17 00:00:00 2001 From: BigUncle Date: Wed, 17 Dec 2025 23:33:17 +0800 Subject: [PATCH] fix(auth): prevent token refresh loop by ignoring timestamp fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add metadataEqualIgnoringTimestamps() function to compare metadata JSON without timestamp/expired/expires_in/last_refresh/access_token fields. This prevents unnecessary file writes when only these fields change during refresh, breaking the fsnotify event → Watcher callback → refresh loop. Key insight: Google OAuth returns a new access_token on each refresh, which was causing file writes and triggering the refresh loop. Fixes antigravity channel excessive log generation issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sdk/auth/filestore.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go index 3c2d60c4..2fa963df 100644 --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -72,7 +72,9 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal) } if existing, errRead := os.ReadFile(path); errRead == nil { - if jsonEqual(existing, raw) { + // Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change. + // This prevents the token refresh loop caused by timestamp/expired/expires_in changes. + if metadataEqualIgnoringTimestamps(existing, raw) { return path, nil } } else if errRead != nil && !os.IsNotExist(errRead) { @@ -264,6 +266,8 @@ func (s *FileTokenStore) baseDirSnapshot() string { return s.baseDir } +// DEPRECATED: Use metadataEqualIgnoringTimestamps for comparing auth metadata. +// This function is kept for backward compatibility but can cause refresh loops. func jsonEqual(a, b []byte) bool { var objA any var objB any @@ -276,6 +280,32 @@ func jsonEqual(a, b []byte) bool { return deepEqualJSON(objA, objB) } +// metadataEqualIgnoringTimestamps compares two metadata JSON blobs, +// ignoring fields that change on every refresh but don't affect functionality. +// This prevents unnecessary file writes that would trigger watcher events and +// create refresh loops. +func metadataEqualIgnoringTimestamps(a, b []byte) bool { + var objA, objB map[string]any + if err := json.Unmarshal(a, &objA); err != nil { + return false + } + if err := json.Unmarshal(b, &objB); err != nil { + return false + } + + // Fields to ignore: these change on every refresh but don't affect authentication logic. + // - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh + // - access_token: Google OAuth returns a new access_token on each refresh, this is expected + // and shouldn't trigger file writes (the new token will be fetched again when needed) + ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"} + for _, field := range ignoredFields { + delete(objA, field) + delete(objB, field) + } + + return deepEqualJSON(objA, objB) +} + func deepEqualJSON(a, b any) bool { switch valA := a.(type) { case map[string]any: