@@ -21,6 +21,12 @@ final class LiveTailViewModel {
2121 var columns: [String] = []
2222 var columnOrder: [String] = []
2323 var hiddenColumns: Set<String> = []
24+ /// The full persisted set of hidden columns, including ones not yet seen in
25+ /// this session. Live-tail columns are discovered incrementally across polls,
26+ /// so `hiddenColumns` (only the seen ones) can't be the source of truth — a
27+ /// column hidden last session would reappear visible when it shows up in a
28+ /// later poll. This retains the intent so it stays hidden when it appears.
29+ private var savedHiddenColumns: Set<String> = []
2430 private var currentStream: String?
2531
2632 // nonisolated(unsafe) so deinit can invalidate the timer.
@@ -37,6 +43,11 @@ final class LiveTailViewModel {
3743 private var allKnownKeys: Set<String> = []
3844 private var consecutiveErrors = 0
3945 private static let maxConsecutiveErrors = 5
46+ /// Guards against overlapping polls: if a poll's network round-trip outlasts
47+ /// the timer interval, the next tick must not start a second concurrent poll
48+ /// (which would let the two interleave and write `lastTimestamp` /
49+ /// `consecutiveErrors` out of order).
50+ private var isPolling = false
4051
4152 deinit {
4253 timer?.invalidate()
@@ -170,6 +181,7 @@ final class LiveTailViewModel {
170181 columns = []
171182 columnOrder = []
172183 hiddenColumns = []
184+ savedHiddenColumns = []
173185 columnFilters = []
174186 currentStream = stream
175187 rebuildFilteredEntries()
@@ -201,6 +213,7 @@ final class LiveTailViewModel {
201213 columns = []
202214 columnOrder = []
203215 hiddenColumns = []
216+ savedHiddenColumns = []
204217 columnFilters = []
205218 rebuildFilteredEntries()
206219 }
@@ -214,10 +227,12 @@ final class LiveTailViewModel {
214227 func toggleColumnVisibility(_ column: String) {
215228 if hiddenColumns.contains(column) {
216229 hiddenColumns.remove(column)
230+ savedHiddenColumns.remove(column)
217231 } else {
218232 let visibleCount = columnOrder.count - hiddenColumns.count
219233 if visibleCount > 1 {
220234 hiddenColumns.insert(column)
235+ savedHiddenColumns.insert(column)
221236 }
222237 }
223238 saveColumnConfig()
@@ -225,6 +240,7 @@ final class LiveTailViewModel {
225240
226241 func showAllColumns() {
227242 hiddenColumns.removeAll()
243+ savedHiddenColumns.removeAll()
228244 saveColumnConfig()
229245 }
230246
@@ -245,6 +261,7 @@ final class LiveTailViewModel {
245261 func resetColumnConfig() {
246262 columnOrder = columns
247263 hiddenColumns.removeAll()
264+ savedHiddenColumns.removeAll()
248265 saveColumnConfig()
249266 }
250267
@@ -256,7 +273,9 @@ final class LiveTailViewModel {
256273
257274 private func saveColumnConfig() {
258275 guard let stream = currentStream else { return }
259− let config = QueryViewModel.ColumnConfiguration(order: columnOrder, hidden: hiddenColumns)
276+ // Persist the full intended hidden set (including columns not yet seen
277+ // this session) so it survives across restarts and incremental discovery.
278+ let config = QueryViewModel.ColumnConfiguration(order: columnOrder, hidden: savedHiddenColumns)
260279 if let data = try? JSONEncoder().encode(config) {
261280 UserDefaults.standard.set(data, forKey: Self.columnConfigKey(for: stream))
262281 }
@@ -298,9 +317,11 @@ final class LiveTailViewModel {
298317 merged.append(col)
299318 }
300319 columnOrder = merged
320+ savedHiddenColumns = config.hidden
301321 hiddenColumns = config.hidden.intersection(extractedSet)
302322 } else {
303323 columnOrder = extracted
324+ savedHiddenColumns = []
304325 hiddenColumns = []
305326 }
306327 } else {
@@ -313,11 +334,18 @@ final class LiveTailViewModel {
313334 if !newCols.isEmpty {
314335 columns.append(contentsOf: newCols)
315336 columnOrder.append(contentsOf: newCols)
337+ // Re-hide any newly appearing column that was hidden in a prior
338+ // session but hadn't been seen yet this run.
339+ hiddenColumns.formUnion(newCols.filter { savedHiddenColumns.contains($0) })
316340 }
317341 }
318342 }
319343
320344 private func poll(client: ParseableClient, stream: String) async {
345+ guard !isPolling else { return }
346+ isPolling = true
347+ defer { isPolling = false }
348+
321349 let now = Date()
322350 // The Parseable server truncates query time-ranges to minute
323351 // boundaries, so a narrow window (< 60 s) can collapse to a