From 772d13c19d88c570ace26d9af2db5b56fec6c540 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 17:54:40 +0100 Subject: [PATCH] fix: handle iOS global agent transcripts --- .../Tests/IOSGatewayChatTransportTests.swift | 2 + .../Sources/OpenClawChatUI/ChatModels.swift | 3 + .../ChatViewModel+SessionKeys.swift | 33 ++++++++- .../OpenClawChatUI/ChatViewModel.swift | 2 +- .../OpenClawKitTests/ChatViewModelTests.swift | 69 +++++++++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) diff --git a/apps/ios/Tests/IOSGatewayChatTransportTests.swift b/apps/ios/Tests/IOSGatewayChatTransportTests.swift index 3ae45ec6acb..09c7cb0f05c 100644 --- a/apps/ios/Tests/IOSGatewayChatTransportTests.swift +++ b/apps/ios/Tests/IOSGatewayChatTransportTests.swift @@ -95,6 +95,7 @@ import Testing @Test func mapsSessionMessageEventToSessionMessage() { let payload = AnyCodable([ "sessionKey": AnyCodable("agent:main:main"), + "agentId": AnyCodable("main"), "messageId": AnyCodable("msg-1"), "messageSeq": AnyCodable(7), "message": AnyCodable([ @@ -119,6 +120,7 @@ import Testing switch mapped { case let .sessionMessage(message): #expect(message.sessionKey == "agent:main:main") + #expect(message.agentId == "main") #expect(message.messageId == "msg-1") #expect(message.messageSeq == 7) #expect(message.message?.role == "assistant") diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift index d349369fdc0..ecb9b1d7cd9 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift @@ -326,17 +326,20 @@ public struct OpenClawChatEventPayload: Codable, Sendable { public struct OpenClawSessionMessageEventPayload: Codable, Sendable { public let sessionKey: String? + public let agentId: String? public let message: OpenClawChatMessage? public let messageId: String? public let messageSeq: Int? public init( sessionKey: String?, + agentId: String? = nil, message: OpenClawChatMessage?, messageId: String?, messageSeq: Int?) { self.sessionKey = sessionKey + self.agentId = agentId self.message = message self.messageId = messageId self.messageSeq = messageSeq diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift index e783c4f21ef..0d086d23f74 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+SessionKeys.swift @@ -8,7 +8,21 @@ extension OpenClawChatViewModel { mainSessionKey: self.resolvedMainSessionKey) } - static func matchesCurrentSessionKey(incoming: String, current: String, mainSessionKey: String) -> Bool { + func matchesCurrentSessionKey(incoming: String, agentId: String?, current: String) -> Bool { + Self.matchesCurrentSessionKey( + incoming: incoming, + agentId: agentId, + current: current, + mainSessionKey: self.resolvedMainSessionKey) + } + + static func matchesCurrentSessionKey( + incoming: String, + agentId: String? = nil, + current: String, + mainSessionKey: String) + -> Bool + { let incomingNormalized = incoming.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let currentNormalized = current.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if incomingNormalized == currentNormalized { @@ -23,6 +37,13 @@ extension OpenClawChatViewModel { { return true } + if Self.matchesSelectedAgentGlobal( + incoming: incomingNormalized, + agentId: agentId, + current: currentNormalized) + { + return true + } return false } @@ -36,4 +57,14 @@ extension OpenClawChatViewModel { return (current == "main" && incoming == "agent:main:main") || (incoming == "main" && current == "agent:main:main") } + + private static func matchesSelectedAgentGlobal(incoming: String, agentId: String?, current: String) -> Bool { + guard incoming == "global", + let selectedAgentId = agentId?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), + !selectedAgentId.isEmpty + else { + return false + } + return current == "agent:\(selectedAgentId):global" + } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index 18948e24a6f..9fcd6722f2b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -1302,7 +1302,7 @@ public final class OpenClawChatViewModel { private func handleSessionMessageEvent(_ payload: OpenClawSessionMessageEventPayload) { if let sessionKey = payload.sessionKey, - !self.matchesCurrentSessionKey(incoming: sessionKey, current: self.sessionKey) + !self.matchesCurrentSessionKey(incoming: sessionKey, agentId: payload.agentId, current: self.sessionKey) { return } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index 7c1d1f5279b..762144123fd 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -1092,6 +1092,75 @@ extension TestChatTransportState { } } + @Test func appendsGlobalSessionUserMessageForSelectedAgent() async throws { + let now = Date().timeIntervalSince1970 * 1000 + let (transport, vm) = await makeViewModel( + sessionKey: "agent:work:global", + historyResponses: [historyPayload(sessionKey: "agent:work:global")]) + + await MainActor.run { vm.load() } + try await waitUntil("bootstrap history loaded") { await MainActor.run { vm.messages.isEmpty } } + + transport.emit( + .sessionMessage( + OpenClawSessionMessageEventPayload( + sessionKey: "global", + agentId: "work", + message: OpenClawChatMessage( + role: "user", + content: [ + OpenClawChatMessageContent( + type: "text", + text: "global transcript", + mimeType: nil, + fileName: nil, + content: nil), + ], + timestamp: now), + messageId: "msg-global-work", + messageSeq: 1))) + + try await waitUntil("selected agent global transcript visible") { + await MainActor.run { + vm.messages.count == 1 && + vm.messages.first?.role == "user" && + vm.messages.first?.content.first?.text == "global transcript" + } + } + } + + @Test func ignoresGlobalSessionUserMessageForDifferentAgent() async throws { + let now = Date().timeIntervalSince1970 * 1000 + let (transport, vm) = await makeViewModel( + sessionKey: "agent:work:global", + historyResponses: [historyPayload(sessionKey: "agent:work:global")]) + + await MainActor.run { vm.load() } + try await waitUntil("bootstrap history loaded") { await MainActor.run { vm.messages.isEmpty } } + + transport.emit( + .sessionMessage( + OpenClawSessionMessageEventPayload( + sessionKey: "global", + agentId: "main", + message: OpenClawChatMessage( + role: "user", + content: [ + OpenClawChatMessageContent( + type: "text", + text: "wrong global transcript", + mimeType: nil, + fileName: nil, + content: nil), + ], + timestamp: now), + messageId: "msg-global-main", + messageSeq: 1))) + + try await Task.sleep(nanoseconds: 100_000_000) + #expect(await MainActor.run { vm.messages.isEmpty }) + } + @Test func ignoresAgentMainSessionMessageForDifferentCurrentMainAlias() async throws { let now = Date().timeIntervalSince1970 * 1000 let (transport, vm) = await makeViewModel(historyResponses: [historyPayload()])