From a1d5dce7abf4ed628f61b4794834b7c880b2ee76 Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:42:56 +0000 Subject: [PATCH] iOS: use dedicated session key for chat sheet (#21139) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 31a27b0c5b27917cae4ec0b8b3fc3f5d8682dd7b 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 | 8 ++++++++ apps/ios/Sources/RootCanvas.swift | 2 +- apps/ios/Tests/NodeAppModelInvokeTests.swift | 13 +++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48f6ebdf7b..0365c6f210a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/Streaming: keep assistant partial streaming active during reasoning streams, handle native `thinking_*` stream events consistently, dedupe mixed reasoning-end signals, and clear stale mutating tool errors after same-target retry success. (#20635) Thanks @obviyus. +- iOS/Chat: use a dedicated iOS chat session key for ChatSheet routing to avoid cross-client session collisions with main-session traffic. (#21139) thanks @mbelinky. - iOS/Chat: auto-resync chat history after reconnect sequence gaps, clear stale pending runs, and avoid dead-end manual refresh errors after transient disconnects. (#21135) thanks @mbelinky. - iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman. - iOS/Onboarding: prevent pairing-status flicker during auto-resume by keeping resumed state transitions stable. (#20310) Thanks @mbelinky. diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 1d09251dd76..ef2f375296b 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1603,6 +1603,14 @@ extension NodeAppModel { return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base) } + var chatSessionKey: String { + let base = "ios" + let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if agentId.isEmpty || (!defaultId.isEmpty && agentId == defaultId) { return base } + return SessionKey.makeAgentSessionKey(agentId: agentId, baseKey: base) + } + var activeAgentName: String { let agentId = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let defaultId = (self.gatewayDefaultAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 70ba9cdb96f..da893d3c943 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -99,7 +99,7 @@ struct RootCanvas: View { ChatSheet( // Chat RPCs run on the operator session (read/write scopes). gateway: self.appModel.operatorSession, - sessionKey: self.appModel.mainSessionKey, + sessionKey: self.appModel.chatSessionKey, agentName: self.appModel.activeAgentName, userAccent: self.appModel.seamColor) case .quickSetup: diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index f5f40fc8b7c..403c08f5c73 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -77,6 +77,19 @@ private final class MockWatchMessagingService: WatchMessagingServicing, @uncheck #expect(json.contains("\"value\"")) } + @Test @MainActor func chatSessionKeyDefaultsToIOSBase() { + let appModel = NodeAppModel() + #expect(appModel.chatSessionKey == "ios") + } + + @Test @MainActor func chatSessionKeyUsesAgentScopedKeyForNonDefaultAgent() { + let appModel = NodeAppModel() + appModel.gatewayDefaultAgentId = "main" + appModel.setSelectedAgentId("agent-123") + #expect(appModel.chatSessionKey == SessionKey.makeAgentSessionKey(agentId: "agent-123", baseKey: "ios")) + #expect(appModel.mainSessionKey == "agent:agent-123:main") + } + @Test @MainActor func handleInvokeRejectsBackgroundCommands() async { let appModel = NodeAppModel() appModel.setScenePhase(.background)