From f5add27d39838d8b363ecd82a4d95a5aa5a7987d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=99=BA=E8=B6=850668000959?= Date: Mon, 8 Jun 2026 16:47:53 +0800 Subject: [PATCH 1/2] fix(evolution): add ok check for LoadOrStore type assertion in lockStoreFile --- pkg/evolution/store.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/evolution/store.go b/pkg/evolution/store.go index 2e7890799..d84110761 100644 --- a/pkg/evolution/store.go +++ b/pkg/evolution/store.go @@ -555,7 +555,16 @@ func isInvalidJSON(err error) bool { func lockStoreFile(path string) func() { actual, _ := storeFileLocks.LoadOrStore(path, &sync.Mutex{}) - mu := actual.(*sync.Mutex) + mu, ok := actual.(*sync.Mutex) + if !ok { + // Delete the corrupted entry so that the next LoadOrStore + // atomically creates a fresh mutex. Multiple goroutines may + // race to Delete, but LoadOrStore ensures they all converge + // on the same mutex before entering the critical section. + storeFileLocks.Delete(path) + actual, _ = storeFileLocks.LoadOrStore(path, &sync.Mutex{}) + mu = actual.(*sync.Mutex) + } mu.Lock() return mu.Unlock } From bbdf746bcba5d77d6dedcc10391aed56be4b9d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E6=99=BA=E8=B6=850668000959?= Date: Fri, 12 Jun 2026 11:25:14 +0800 Subject: [PATCH 2/2] fix(evolution): use CompareAndSwap for atomic lockStoreFile repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Delete+LoadOrStore pattern with CompareAndSwap to atomically swap in a fresh mutex when a corrupted entry is detected. Changes: - Use a retry loop with CAS instead of Delete+LoadOrStore, avoiding the race where two goroutines could create different mutexes - Add nil check: a nil *sync.Mutex passes the type assertion but panics on Lock() — now handled by the CAS recovery path - Use continue-loop retry instead of unchecked second assertion CompareAndSwap guarantees that when multiple goroutines detect a corrupted entry, they converge on the same replacement mutex. --- pkg/evolution/store.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pkg/evolution/store.go b/pkg/evolution/store.go index d84110761..3ab7af4d6 100644 --- a/pkg/evolution/store.go +++ b/pkg/evolution/store.go @@ -554,19 +554,20 @@ func isInvalidJSON(err error) bool { } func lockStoreFile(path string) func() { - actual, _ := storeFileLocks.LoadOrStore(path, &sync.Mutex{}) - mu, ok := actual.(*sync.Mutex) - if !ok { - // Delete the corrupted entry so that the next LoadOrStore - // atomically creates a fresh mutex. Multiple goroutines may - // race to Delete, but LoadOrStore ensures they all converge - // on the same mutex before entering the critical section. - storeFileLocks.Delete(path) - actual, _ = storeFileLocks.LoadOrStore(path, &sync.Mutex{}) - mu = actual.(*sync.Mutex) + for { + actual, _ := storeFileLocks.LoadOrStore(path, &sync.Mutex{}) + mu, ok := actual.(*sync.Mutex) + if !ok || mu == nil { + // Corrupted entry (wrong type or nil *sync.Mutex). + // Atomically swap in a fresh mutex via CompareAndSwap. + // If CAS fails, another goroutine already replaced it — + // just retry the loop to pick up the valid entry. + storeFileLocks.CompareAndSwap(path, actual, &sync.Mutex{}) + continue + } + mu.Lock() + return mu.Unlock } - mu.Lock() - return mu.Unlock } func (s *Store) profilePath(workspaceID, skillName string) (string, error) {