@@ -48,6 +48,10 @@ final class LiveTailViewModel {
4848 /// (which would let the two interleave and write `lastTimestamp` /
4949 /// `consecutiveErrors` out of order).
5050 private var isPolling = false
51+ /// Bumped by start()/stop()/clear() so an in-flight poll whose network
52+ /// round-trip outlives one of those transitions can detect it was superseded
53+ /// and drop its results instead of repopulating a torn-down/cleared tail.
54+ private var pollGeneration = 0
5155
5256 deinit {
5357 timer?.invalidate()
@@ -56,6 +60,9 @@ final class LiveTailViewModel {
5660 // Cached formatters — nonisolated(unsafe) so static methods can use them from Task.detached
5761 nonisolated(unsafe) private static let displayFormatter: DateFormatter = {
5862 let f = DateFormatter()
63+ // Fixed-format formatters must use en_US_POSIX so a non-Gregorian or
64+ // non-Latin-digit system locale doesn't render wrong numerals/era.
65+ f.locale = Locale(identifier: "en_US_POSIX")
5966 f.dateFormat = "HH:mm:ss.SSS"
6067 return f
6168 }()
@@ -168,6 +175,7 @@ final class LiveTailViewModel {
168175 let storedMax = UserDefaults.standard.integer(forKey: "liveTailMaxEntries")
169176 if storedMax > 0 { maxEntries = storedMax }
170177
178+ pollGeneration += 1
171179 isRunning = true
172180 isPaused = false
173181 errorMessage = nil
@@ -195,6 +203,7 @@ final class LiveTailViewModel {
195203 }
196204
197205 func stop() {
206+ pollGeneration += 1
198207 timer?.invalidate()
199208 timer = nil
200209 isRunning = false
@@ -206,8 +215,14 @@ final class LiveTailViewModel {
206215 }
207216
208217 func clear() {
218+ pollGeneration += 1
209219 entries = []
210220 seenFingerprints = []
221+ // Advance the look-back floor to now: without this, the next poll queries
222+ // from the old `lastTimestamp` with an empty fingerprint set and re-inserts
223+ // every just-cleared record still inside the ~90 s window, repopulating the
224+ // view the user just emptied. Matches start()'s reset.
225+ lastTimestamp = Date()
211226 allKnownKeys = []
212227 droppedCount = 0
213228 columns = []
@@ -355,6 +370,7 @@ final class LiveTailViewModel {
355370 guard !isPolling else { return }
356371 isPolling = true
357372 defer { isPolling = false }
373+ let generation = pollGeneration
358374
359375 let now = Date()
360376 // The Parseable server truncates query time-ranges to minute
@@ -374,6 +390,11 @@ final class LiveTailViewModel {
374390 do {
375391 let records = try await client.query(sql: sql, startTime: queryStart, endTime: now)
376392
393+ // A stop()/clear()/start during the network round-trip supersedes this
394+ // poll; don't write stale results to a torn-down or cleared tail. Also
395+ // bail if the user paused mid-flight.
396+ guard isRunning, !isPaused, generation == pollGeneration else { return }
397+
377398 // Process records off the main actor using a snapshot of seen fingerprints
378399 let seenSnapshot = seenFingerprints
379400 let pollTime = now
@@ -395,6 +416,10 @@ final class LiveTailViewModel {
395416 return results
396417 }.value
397418
419+ // Re-check after the detached processing hop: a stop/clear could have
420+ // landed while we were off the main actor.
421+ guard isRunning, !isPaused, generation == pollGeneration else { return }
422+
398423 // Authoritative dedup on main actor
399424 var newEntries: [LiveTailEntry] = []
400425 for entry in candidateEntries {
@@ -451,7 +476,10 @@ final class LiveTailViewModel {
451476 errorMessage = nil
452477 consecutiveErrors = 0
453478 } catch {
454− guard !Task.isCancelled else { return }
479+ // Drop the error if this poll was cancelled or superseded by a
480+ // stop/clear/start — don't bump the failure counter or surface an
481+ // error on a tail the user already tore down.
482+ guard !Task.isCancelled, isRunning, generation == pollGeneration else { return }
455483 consecutiveErrors += 1
456484 if consecutiveErrors >= Self.maxConsecutiveErrors {
457485 errorMessage = "\(ParseableError.userFriendlyMessage(for: error)) — stopped after \(consecutiveErrors) consecutive failures"