@@ -225,6 +225,10 @@ final class LiveTailViewModel {
225225 lastTimestamp = Date()
226226 allKnownKeys = []
227227 droppedCount = 0
228+ // Reset the failure counter too: the timer keeps polling after a clear, so
229+ // leaving accumulated errors in place could trip the auto-stop threshold
230+ // sooner than the intended N consecutive failures. Matches start()'s reset.
231+ consecutiveErrors = 0
228232 columns = []
229233 columnOrder = []
230234 hiddenColumns = []
@@ -513,6 +517,19 @@ final class LiveTailViewModel {
513517 return String(h0, radix: 36) + String(h1, radix: 36)
514518 }
515519
520+ /// Mixes the 8 bytes of a number's canonical 64-bit representation, prefixed by
521+ /// a shared numeric type byte so ints and equal-valued doubles fingerprint alike.
522+ nonisolated private static func hashNumberBits(_ bitPattern: UInt64, h0: inout UInt64, h1: inout UInt64) {
523+ h0 = (h0 ^ 0x02) &* 1099511628211
524+ h1 = (h1 ^ 0x02) &* 6700417
525+ var bits = bitPattern
526+ for _ in 0..<8 {
527+ h0 = (h0 ^ (bits & 0xFF)) &* 1099511628211
528+ h1 = (h1 ^ (bits & 0xFF)) &* 6700417
529+ bits >>= 8
530+ }
531+ }
532+
516533 /// Recursively hashes a JSONValue tree without allocating intermediate strings.
517534 /// Uses type-discriminator bytes to prevent cross-type collisions.
518535 nonisolated private static func hashJSONValue(_ value: JSONValue, h0: inout UInt64, h1: inout UInt64) {
@@ -526,24 +543,15 @@ final class LiveTailViewModel {
526543 let byte: UInt64 = b ? 1 : 0
527544 h0 = (h0 ^ byte) &* 1099511628211
528545 h1 = (h1 ^ byte) &* 6700417
546+ // `.int` and `.double` share one numeric encoding so the fingerprint stays
547+ // consistent with JSONValue equality, where `.int(5) == .double(5.0)`.
548+ // JSONValue.hash(into:) canonicalizes both through `Double`; mirror that here
549+ // (with NaN folded to a single bit pattern) so a field that serializes as `5`
550+ // in one poll and `5.0` in another doesn't bypass dedup as a phantom row.
529551 case .int(let i):
530− h0 = (h0 ^ 0x02) &* 1099511628211
531− h1 = (h1 ^ 0x02) &* 6700417
532− var bits = UInt64(bitPattern: Int64(i))
533− for _ in 0..<8 {
534− h0 = (h0 ^ (bits & 0xFF)) &* 1099511628211
535− h1 = (h1 ^ (bits & 0xFF)) &* 6700417
536− bits >>= 8
537− }
552+ hashNumberBits(Double(i).bitPattern, h0: &h0, h1: &h1)
538553 case .double(let d):
539− h0 = (h0 ^ 0x03) &* 1099511628211
540− h1 = (h1 ^ 0x03) &* 6700417
541− var bits = d.bitPattern
542− for _ in 0..<8 {
543− h0 = (h0 ^ UInt64(bits & 0xFF)) &* 1099511628211
544− h1 = (h1 ^ UInt64(bits & 0xFF)) &* 6700417
545− bits >>= 8
546− }
554+ hashNumberBits((d.isNaN ? Double.nan : d).bitPattern, h0: &h0, h1: &h1)
547555 case .string(let s):
548556 h0 = (h0 ^ 0x04) &* 1099511628211
549557 h1 = (h1 ^ 0x04) &* 6700417