@@ -225,6 +225,10 @@ final class LiveTailViewModel {
225 lastTimestamp = Date()
225 lastTimestamp = Date()
226 allKnownKeys = []
226 allKnownKeys = []
227 droppedCount = 0
227 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
228 columns = []
232 columns = []
229 columnOrder = []
233 columnOrder = []
230 hiddenColumns = []
234 hiddenColumns = []
@@ -513,6 +517,19 @@ final class LiveTailViewModel {
513 return String(h0, radix: 36) + String(h1, radix: 36)
517 return String(h0, radix: 36) + String(h1, radix: 36)
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
527+ h0 = (h0 ^ (bits & 0xFF)) &* 1099511628211
528+ h1 = (h1 ^ (bits & 0xFF)) &* 6700417
516 /// Recursively hashes a JSONValue tree without allocating intermediate strings.
533 /// Recursively hashes a JSONValue tree without allocating intermediate strings.
517 /// Uses type-discriminator bytes to prevent cross-type collisions.
534 /// Uses type-discriminator bytes to prevent cross-type collisions.
518 nonisolated private static func hashJSONValue(_ value: JSONValue, h0: inout UInt64, h1: inout UInt64) {
535 nonisolated private static func hashJSONValue(_ value: JSONValue, h0: inout UInt64, h1: inout UInt64) {
@@ -526,24 +543,15 @@ final class LiveTailViewModel {
526 let byte: UInt64 = b ? 1 : 0
543 let byte: UInt64 = b ? 1 : 0
527 h0 = (h0 ^ byte) &* 1099511628211
544 h0 = (h0 ^ byte) &* 1099511628211
528 h1 = (h1 ^ byte) &* 6700417
545 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.
529 case .int(let i):
551 case .int(let i):
530− h0 = (h0 ^ 0x02) &* 1099511628211
552+ hashNumberBits(Double(i).bitPattern, h0: &h0, h1: &h1)
531− h1 = (h1 ^ 0x02) &* 6700417
532− var bits = UInt64(bitPattern: Int64(i))
534− h0 = (h0 ^ (bits & 0xFF)) &* 1099511628211
535− h1 = (h1 ^ (bits & 0xFF)) &* 6700417
538 case .double(let d):
553 case .double(let d):
539− h0 = (h0 ^ 0x03) &* 1099511628211
554+ hashNumberBits((d.isNaN ? Double.nan : d).bitPattern, h0: &h0, h1: &h1)
540− h1 = (h1 ^ 0x03) &* 6700417
541− var bits = d.bitPattern
543− h0 = (h0 ^ UInt64(bits & 0xFF)) &* 1099511628211
544− h1 = (h1 ^ UInt64(bits & 0xFF)) &* 6700417
547 case .string(let s):
555 case .string(let s):
548 h0 = (h0 ^ 0x04) &* 1099511628211
556 h0 = (h0 ^ 0x04) &* 1099511628211
549 h1 = (h1 ^ 0x04) &* 6700417
557 h1 = (h1 ^ 0x04) &* 6700417