diff --git a/CHANGELOG.md b/CHANGELOG.md index e48f6ebdf7..0365c6f210 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 1d09251dd7..ef2f375296 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 70ba9cdb96..da893d3c94 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 f5f40fc8b7..403c08f5c7 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)