






















@@ -188,6 +188,7 @@ final class NodeAppModel {
188188@ObservationIgnored private var backgroundGraceTaskTimer: Task<Void, Never>?
189189private var backgroundReconnectSuppressed = false
190190private var backgroundReconnectLeaseUntil: Date?
191+@ObservationIgnored private var foregroundGatewayResumeCheckInFlight = false
191192private var lastSignificantLocationWakeAt: Date?
192193@ObservationIgnored private let watchReplyCoordinator = WatchReplyCoordinator()
193194private var watchExecApprovalPromptsByID: [String: ExecApprovalPrompt] = [:]
@@ -214,6 +215,7 @@ final class NodeAppModel {
214215private static let watchExecApprovalBridgeStateKey = "watch.execApproval.bridge.state.v1"
215216private static let backgroundAliveLastSuccessAtMsKey = "gateway.backgroundAlive.lastSuccessAtMs"
216217private static let backgroundAliveLastTriggerKey = "gateway.backgroundAlive.lastTrigger"
218+private static let foregroundResumeHealthTimeoutSeconds = 1
217219218220var cameraHUDText: String?
219221var cameraHUDKind: CameraHUDKind?
@@ -417,9 +419,7 @@ final class NodeAppModel {
417419self.isBackgrounded = false
418420self.endBackgroundConnectionGracePeriod(reason: "scene_foreground")
419421self.clearBackgroundReconnectSuppression(reason: "scene_foreground")
420-if self.operatorConnected {
421-self.startGatewayHealthMonitor()
422-}
422+var shouldStartGatewayHealthMonitor = self.operatorConnected
423423if phase == .active {
424424self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: self.backgroundVoiceWakeSuspended)
425425self.backgroundVoiceWakeSuspended = false
@@ -444,6 +444,8 @@ final class NodeAppModel {
444444 // iOS may suspend network sockets in background without a clean close.
445445 // On foreground, force a fresh handshake to avoid "connected but dead" states.
446446if backgroundedFor >= 3.0 {
447+ shouldStartGatewayHealthMonitor = false
448+self.foregroundGatewayResumeCheckInFlight = true
447449Task { [weak self] in
448450guard let self else { return }
449451let operatorWasConnected = await MainActor.run { self.operatorConnected }
@@ -452,31 +454,26 @@ final class NodeAppModel {
452454let healthy = await (try? self.operatorGateway.request(
453455 method: "health",
454456 paramsJSON: nil,
455- timeoutSeconds: 2)) != nil
457+ timeoutSeconds: Self.foregroundResumeHealthTimeoutSeconds)) != nil
456458if healthy {
457-await MainActor.run { self.startGatewayHealthMonitor() }
459+await MainActor.run {
460+self.foregroundGatewayResumeCheckInFlight = false
461+self.startGatewayHealthMonitor()
462+}
458463return
459464}
460465}
461466462-await self.operatorGateway.disconnect()
463-await self.nodeGateway.disconnect()
464467await MainActor.run {
465-guard !self.isAppleReviewDemoModeEnabled else { return }
466-self.setOperatorConnected(false)
467-self.gatewayConnected = false
468- // Foreground recovery must actively restart the saved gateway config.
469- // Disconnecting stale sockets alone can leave us idle if the old
470- // reconnect tasks were suppressed or otherwise got stuck in background.
471-self.gatewayStatusText = "Reconnecting…"
472-self.talkMode.updateGatewayConnected(false)
473-if let cfg = self.activeGatewayConnectConfig {
474-self.applyGatewayConnectConfig(cfg)
475-}
468+self.foregroundGatewayResumeCheckInFlight = false
476469}
470+await self.restartGatewaySessionsAfterForegroundStaleConnection()
477471}
478472}
479473}
474+if shouldStartGatewayHealthMonitor {
475+self.startGatewayHealthMonitor()
476+}
480477@unknown default:
481478self.isBackgrounded = false
482479self.endBackgroundConnectionGracePeriod(reason: "scene_unknown")
@@ -786,6 +783,12 @@ final class NodeAppModel {
786783787784func refreshGatewayOverviewIfConnected() async {
788785guard await self.isOperatorConnected() else { return }
786+if self.foregroundGatewayResumeCheckInFlight {
787+GatewayDiagnostics.log("gateway overview refresh deferred reason=foreground_resume_check")
788+try? await Task.sleep(
789+ nanoseconds: UInt64(Self.foregroundResumeHealthTimeoutSeconds) * 1_000_000_000)
790+guard await self.isOperatorConnected(), !self.foregroundGatewayResumeCheckInFlight else { return }
791+}
789792await self.refreshBrandingFromGateway()
790793await self.refreshAgentsFromGateway()
791794}
@@ -1986,12 +1989,33 @@ extension NodeAppModel {
19861989}
1987199019881991func resetGatewaySessionsForForcedReconnect() async {
1989-self.nodeGatewayTask?.cancel()
1992+let nodeGatewayTask = self.nodeGatewayTask
1993+let operatorGatewayTask = self.operatorGatewayTask
1994+ nodeGatewayTask?.cancel()
19901995self.nodeGatewayTask = nil
1991-self.operatorGatewayTask?.cancel()
1996+ operatorGatewayTask?.cancel()
19921997self.operatorGatewayTask = nil
19931998await self.operatorGateway.disconnect()
19941999await self.nodeGateway.disconnect()
2000+ // Foreground recovery reuses the same config immediately after reset.
2001+ // Wait for canceled loops so their shutdown cleanup cannot clobber the new reconnect state.
2002+if let operatorGatewayTask {
2003+await operatorGatewayTask.value
2004+}
2005+if let nodeGatewayTask {
2006+await nodeGatewayTask.value
2007+}
2008+}
2009+2010+private func restartGatewaySessionsAfterForegroundStaleConnection() async {
2011+await self.resetGatewaySessionsForForcedReconnect()
2012+guard !self.isAppleReviewDemoModeEnabled else { return }
2013+self.setOperatorConnected(false)
2014+self.gatewayConnected = false
2015+self.gatewayStatusText = "Reconnecting…"
2016+self.talkMode.updateGatewayConnected(false)
2017+guard let cfg = self.activeGatewayConnectConfig else { return }
2018+self.applyGatewayConnectConfig(cfg, forceReconnect: true)
19952019}
1996202019972021func disconnectGateway() {
@@ -4826,6 +4850,10 @@ extension NodeAppModel {
48264850(self.nodeGatewayTask != nil, self.operatorGatewayTask != nil)
48274851}
482848524853+func _test_restartGatewaySessionsAfterForegroundStaleConnection() async {
4854+await self.restartGatewaySessionsAfterForegroundStaleConnection()
4855+}
4856+48294857func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
48304858await self.handleSuccessfulBootstrapGatewayOnboarding(
48314859 url: URL(string: "wss://gateway.example")!,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。