From 8a478a9f31403dc3c063604dbaa4769d0e1d6d1a Mon Sep 17 00:00:00 2001 From: Kesley DEV Date: Wed, 26 Aug 2026 23:34:20 -0300 Subject: [PATCH] fix(ios): a linger sleeper may not close a window it does not own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit endLinger() does not cancel the 25s sleeper, so leaving and returning near the end of one background window lets the first sleeper wake during the second and end it early — dropping the stream while the user still expects the grace period, so an approval arriving in those seconds never lands. The UIKit expiration handler carried no window identity either. A generation counter, incremented only when a window opens, is captured by both and checked on wake. Session is @MainActor, so there is no suspension between the guard and disconnect() for the check to race. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SkWZ3y7iKQMjH6tHNFBte7 --- ios/App/Session.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ios/App/Session.swift b/ios/App/Session.swift index 304141e7d..1f9efb07f 100644 --- a/ios/App/Session.swift +++ b/ios/App/Session.swift @@ -376,6 +376,9 @@ final class Session: ObservableObject { } private var lingerTask: UIBackgroundTaskIdentifier = .invalid + /// Identifies the background window currently allowed to end the stream. + /// A sleeper from an earlier window may wake after a newer one has opened. + private var lingerGeneration = 0 /// Leaving the screen: keep the stream alive for the grace period iOS /// allows (~30 s) rather than cutting it at once, so an approval that @@ -384,13 +387,22 @@ final class Session: ObservableObject { /// the cursor is written down at a known point. func linger() { guard streamTask != nil, lingerTask == .invalid else { disconnect(); return } + lingerGeneration += 1 + let generation = lingerGeneration lingerTask = UIApplication.shared.beginBackgroundTask(withName: "companion.linger") { [weak self] in // time is up before our own timer — the system wants us gone now - self?.disconnect() + guard let self, + self.lingerGeneration == generation, + self.lingerTask != .invalid + else { return } + self.disconnect() } Task { [weak self] in try? await Task.sleep(for: .seconds(25)) - guard let self, self.lingerTask != .invalid else { return } + guard let self, + self.lingerGeneration == generation, + self.lingerTask != .invalid + else { return } self.disconnect() } }