Skip to content

Commit a6c3bd3

Browse files
saltukalakusclaude
andcommitted
fix: prevent sendMessage() auto-reconnect on intentional background disconnect
Add isIntentionallyDisconnecting flag to ACPClientWrapper. Set in disconnect(), cleared in connect(). Guards the sendMessage() reconnect loop so backgrounding the app (which calls disconnectAll()) no longer creates a second WebSocket connection racing AgentManager's reconnect on foreground — which was preventing push notifications from firing. Also adds scenePhase handler in AptoveApp: background closes all WebSocket connections (with UIBackgroundTask to complete the close frame), active reconnects after 0.5s delay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f5db3b1 commit a6c3bd3

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

Aptove/AptoveApp.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import SwiftUI
22
import UserNotifications
3+
import UIKit
34

45
/// AppDelegate for handling push notification registration
56
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
@@ -72,14 +73,26 @@ struct AptoveApp: App {
7273
.onChange(of: scenePhase) { _, newPhase in
7374
switch newPhase {
7475
case .active:
75-
print("🌅 [push-dbg] App became ACTIVE — reconnecting agents")
76-
agentManager.autoConnectAllAgents()
76+
print("🌅 [push-dbg] App became ACTIVE — reconnecting agents (0.5s delay for in-flight disconnect)")
77+
// Small delay so any in-flight background disconnect completes before
78+
// we reconnect, preventing two simultaneous connections on the bridge.
79+
Task {
80+
try? await Task.sleep(nanoseconds: 500_000_000)
81+
agentManager.autoConnectAllAgents()
82+
}
7783
case .inactive:
7884
print("🌄 [push-dbg] App became INACTIVE (transitioning)")
7985
case .background:
8086
print("🌙 [push-dbg] App entered BACKGROUND — closing WebSocket so bridge detects disconnect and pushes")
87+
// Request background execution time so iOS doesn't suspend the process
88+
// before the WebSocket close frame is delivered to the bridge.
89+
var bgTaskId = UIBackgroundTaskIdentifier.invalid
90+
bgTaskId = UIApplication.shared.beginBackgroundTask(withName: "ws-background-disconnect") {
91+
UIApplication.shared.endBackgroundTask(bgTaskId)
92+
}
8193
Task {
8294
await agentManager.disconnectAll()
95+
UIApplication.shared.endBackgroundTask(bgTaskId)
8396
}
8497
@unknown default:
8598
break

Aptove/Services/ACPClientWrapper.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ class ACPClientWrapper: ObservableObject {
190190
}
191191

192192
func connect(existingSessionId: String?) async {
193+
// An intentional disconnect is over — allow sendMessage() to reconnect again.
194+
isIntentionallyDisconnecting = false
193195
print("🔌 ACPClientWrapper.connect(): Starting connection flow...")
194196
if let sessionId = existingSessionId {
195197
print("🔌 ACPClientWrapper.connect(): Will try to load session: \(sessionId)")
@@ -427,6 +429,8 @@ class ACPClientWrapper: ObservableObject {
427429
}
428430

429431
func disconnect() async {
432+
// Signal in-flight sendMessage() Tasks to skip their reconnect loop.
433+
isIntentionallyDisconnecting = true
430434
// Cancel the transport observer first so an intentional disconnect
431435
// does not trigger the unexpected-disconnect callback.
432436
transportObserverTask?.cancel()
@@ -585,6 +589,10 @@ class ACPClientWrapper: ObservableObject {
585589
var onUnexpectedDisconnect: (() -> Void)?
586590
private var transportObserverTask: Task<Void, Never>?
587591

592+
/// Set to true by `disconnect()` to suppress the sendMessage() internal reconnect
593+
/// when the disconnect is intentional (e.g. app backgrounding). Cleared by `connect()`.
594+
private var isIntentionallyDisconnecting = false
595+
588596
/// Called when another device sends a user message to the same session.
589597
/// The string is the plain text of the remote message.
590598
var onRemoteUserMessage: ((String) -> Void)?
@@ -723,6 +731,17 @@ class ACPClientWrapper: ObservableObject {
723731
return
724732
}
725733

734+
// Skip reconnect if disconnect() was called intentionally (e.g. app backgrounding).
735+
// AgentManager will reconnect via autoConnectAllAgents() when the app foregrounds.
736+
if await MainActor.run(body: { self.isIntentionallyDisconnecting }) {
737+
print("🔄 Skipping auto-reconnect — intentional disconnect in progress")
738+
await MainActor.run {
739+
self.connectionState = .disconnected
740+
onComplete(nil, error.localizedDescription)
741+
}
742+
return
743+
}
744+
726745
// If the agent says the session doesn't exist, clear it
727746
// so the reconnect creates a fresh session instead of
728747
// retrying the same stale session ID in a loop.

0 commit comments

Comments
 (0)