From fc65f70a9b97b5af99f6a82030145ad16897c6dc Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:23:06 +0000 Subject: [PATCH] iOS: stabilize pairing/reconnect loops (#20056) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: b01a482a17ea440842ef8f7e9b853a307d8a67b6 Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky --- CHANGELOG.md | 1 + apps/ios/Sources/Model/NodeAppModel.swift | 7 ++++++- apps/ios/Sources/Onboarding/OnboardingWizardView.swift | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aff7b56dcc..5dd774e2d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky. - Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky. - Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`:topic:`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi. - iOS/Signing: restore local auto-selected signing-team overrides during iOS project generation by wiring `.local-signing.xcconfig` into the active signing config and emitting `OPENCLAW_DEVELOPMENT_TEAM` in local signing setup. (#19993) Thanks @ngutman. diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 8297a22b7f9..7b843d154a0 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -584,8 +584,11 @@ final class NodeAppModel { onFailure: { [weak self] _ in guard let self else { return } await self.operatorGateway.disconnect() + await self.nodeGateway.disconnect() await MainActor.run { self.operatorConnected = false + self.gatewayConnected = false + self.gatewayStatusText = "Reconnecting…" self.talkMode.updateGatewayConnected(false) } }) @@ -1928,7 +1931,9 @@ private extension NodeAppModel { clientId: clientId, clientMode: "ui", clientDisplayName: displayName, - includeDeviceIdentity: true) + // Operator traffic should authenticate via shared gateway auth only. + // Including device identity here can trigger duplicate pairing flows. + includeDeviceIdentity: false) } func legacyClientIdFallback(currentClientId: String, error: Error) -> String? { diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index f2d2cca6357..2dda00f1852 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -675,6 +675,11 @@ struct OnboardingWizardView: View { // We intentionally stop reconnect churn while unpaired to avoid generating multiple pending requests. self.appModel.gatewayAutoReconnectEnabled = true self.appModel.gatewayPairingPaused = false + self.appModel.gatewayPairingRequestId = nil + // Pairing state is sticky to prevent UI flip-flop during reconnect churn. + // Once the user explicitly resumes after approving, clear the sticky issue + // so new status/auth errors can surface instead of being masked as pairing. + self.issue = .none self.connectMessage = "Retrying after approval…" self.statusLine = "Retrying after approval…" Task { await self.retryLastAttempt() }