@@ -34,6 +34,9 @@ final class LiveTailViewModel {
3434 nonisolated(unsafe) private var timer: Timer?
3535 private var lastTimestamp: Date?
3636 private var seenFingerprints: Set<String> = []
37+ /// Insertion-ordered mirror of `seenFingerprints` (oldest first), used to
38+ /// evict the least-recently-seen fingerprints when the set is trimmed.
39+ private var seenFingerprintOrder: [String] = []
3740 /// Upper bound on the dedup fingerprint set. Kept well above `maxEntries` so
3841 /// fingerprints of recently-evicted entries survive long enough to suppress
3942 /// duplicates that reappear inside the next poll's look-back window, while
@@ -181,6 +184,7 @@ final class LiveTailViewModel {
181184 errorMessage = nil
182185 entries = []
183186 seenFingerprints = []
187+ seenFingerprintOrder = []
184188 allKnownKeys = []
185189 droppedCount = 0
186190 consecutiveErrors = 0
@@ -218,6 +222,7 @@ final class LiveTailViewModel {
218222 pollGeneration += 1
219223 entries = []
220224 seenFingerprints = []
225+ seenFingerprintOrder = []
221226 // Advance the look-back floor to now: without this, the next poll queries
222227 // from the old `lastTimestamp` with an empty fingerprint set and re-inserts
223228 // every just-cleared record still inside the ~90 s window, repopulating the
@@ -428,6 +433,7 @@ final class LiveTailViewModel {
428433 var newEntries: [LiveTailEntry] = []
429434 for entry in candidateEntries {
430435 if seenFingerprints.insert(entry.fingerprint).inserted {
436+ seenFingerprintOrder.append(entry.fingerprint)
431437 newEntries.append(entry)
432438 }
433439 }
@@ -452,10 +458,22 @@ final class LiveTailViewModel {
452458 // duplicate. Keep the full seen-set so dedup stays correct;
453459 // it is bounded below to avoid unbounded growth.
454460 if seenFingerprints.count > maxSeenFingerprints {
455− // Retain the fingerprints of everything still buffered
456− // (those are the ones that matter for dedup) and discard
457− // the older surplus.
458− seenFingerprints = Set(entries.map { $0.fingerprint })
461+ // Evict the OLDEST surplus fingerprints by insertion order,
462+ // not by what's currently buffered. Collapsing to only the
463+ // buffered set would drop recently-evicted fingerprints that
464+ // still fall inside the next poll's ~90 s look-back window,
465+ // re-admitting those records as duplicates. Keeping the
466+ // most-recently-seen tail preserves a full window of evicted
467+ // fingerprints. Buffered fingerprints are always retained so
468+ // a re-fetch of a buffered record stays deduplicated.
469+ let overflow = seenFingerprints.count - maxSeenFingerprints
470+ let buffered = Set(entries.map { $0.fingerprint })
471+ let toDrop = Set(seenFingerprintOrder.prefix(overflow))
472+ .subtracting(buffered)
473+ if !toDrop.isEmpty {
474+ seenFingerprints.subtract(toDrop)
475+ seenFingerprintOrder.removeAll { toDrop.contains($0) }
476+ }
459477 }
460478 }
461479