@@ -17,6 +17,8 @@ final class QueryViewModel {
1717 // Build search texts asynchronously
1818 searchTextTask?.cancel()
1919 cachedSearchTexts = []
20+ searchTextGeneration &+= 1
21+ let generation = searchTextGeneration
2022 // Apply the current filter immediately using the inline fallback path
2123 // (the cache was just invalidated). Calling updateFilteredResults()
2224 // rather than `filteredResults = results` avoids briefly showing
@@ -36,6 +38,12 @@ final class QueryViewModel {
3638 }
3739 }.value
3840 guard let self, !Task.isCancelled else { return }
41+ // Reject a stale write: if `results` was reassigned while this task
42+ // was awaiting, a newer didSet bumped the generation. Task.isCancelled
43+ // alone isn't sufficient because two re-queries producing result sets
44+ // of identical count would otherwise let this stale, same-length
45+ // cachedSearchTexts slip past updateFilteredResults's count guard.
46+ guard generation == self.searchTextGeneration else { return }
3947 self.cachedSearchTexts = texts
4048 if !self.filterText.isEmpty {
4149 self.updateFilteredResults()
@@ -45,6 +53,9 @@ final class QueryViewModel {
4553 }
4654 private var cachedSearchTexts: [String] = []
4755 private var searchTextTask: Task<Void, Never>?
56+ /// Bumped on every `results` reassignment so the async search-text builder can
57+ /// detect it was superseded and drop a stale write (see results.didSet).
58+ private var searchTextGeneration = 0
4859 var columns: [String] = []
4960 var columnOrder: [String] = []
5061 var hiddenColumns: Set<String> = []