fix(watch): preserve snapshot compatibility

This commit is contained in:
Vincent Koc
2026-07-12 14:31:16 +02:00
parent 884e553a36
commit ec65e10df7
3 changed files with 146 additions and 12 deletions

View File

@@ -436,6 +436,42 @@ struct WatchAppSnapshotMessage: Codable, Equatable {
var sentAtMs: Int64?
var snapshotId: String?
private init(
gatewayStatus: OpenClawWatchAppStatus,
gatewayConnected: Bool,
agentName: String,
agentAvatarURL: String?,
agentAvatarText: String?,
sessionKey: String,
gatewayStableID: String?,
talkStatus: OpenClawWatchAppStatus,
talkEnabled: Bool,
talkListening: Bool,
talkSpeaking: Bool,
pendingApprovalCount: Int,
chatItems: [WatchChatItem]?,
chatStatus: OpenClawWatchAppStatus?,
sentAtMs: Int64?,
snapshotId: String?)
{
self.gatewayStatus = gatewayStatus
self.gatewayConnected = gatewayConnected
self.agentName = agentName
self.agentAvatarURL = agentAvatarURL
self.agentAvatarText = agentAvatarText
self.sessionKey = sessionKey
self.gatewayStableID = gatewayStableID
self.talkStatus = talkStatus
self.talkEnabled = talkEnabled
self.talkListening = talkListening
self.talkSpeaking = talkSpeaking
self.pendingApprovalCount = pendingApprovalCount
self.chatItems = chatItems
self.chatStatus = chatStatus
self.sentAtMs = sentAtMs
self.snapshotId = snapshotId
}
static func parsePayload(_ payload: [String: Any]) -> Self? {
guard let type = payload["type"] as? String,
type == WatchPayloadType.appSnapshot.rawValue
@@ -548,21 +584,21 @@ struct WatchAppSnapshotMessage: Codable, Equatable {
self.chatItems = try container.decodeIfPresent([WatchChatItem].self, forKey: .chatItems)
self.sentAtMs = try container.decodeIfPresent(Int64.self, forKey: .sentAtMs)
self.snapshotId = try container.decodeIfPresent(String.self, forKey: .snapshotId)
self.gatewayStatus = try container.decodeIfPresent(
self.gatewayStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .gatewayStatus) ?? Self.decodeLegacyGatewayStatus(
forKey: .gatewayStatus)) ?? Self.decodeLegacyGatewayStatus(
text: container.decodeIfPresent(String.self, forKey: .gatewayStatusText),
connected: self.gatewayConnected)
self.talkStatus = try container.decodeIfPresent(
self.talkStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .talkStatus) ?? Self.decodeLegacyTalkStatus(
forKey: .talkStatus)) ?? Self.decodeLegacyTalkStatus(
text: container.decodeIfPresent(String.self, forKey: .talkStatusText),
enabled: self.talkEnabled,
listening: self.talkListening,
speaking: self.talkSpeaking)
self.chatStatus = try container.decodeIfPresent(
self.chatStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .chatStatus) ?? Self.decodeLegacyChatStatus(
forKey: .chatStatus)) ?? Self.decodeLegacyChatStatus(
code: container.decodeIfPresent(String.self, forKey: .chatStatusCode),
text: container.decodeIfPresent(String.self, forKey: .chatStatusText))
}

View File

@@ -384,6 +384,53 @@ public struct OpenClawWatchAppSnapshotMessage: Codable, Sendable, Equatable {
self.snapshotId = snapshotId
}
public init(
gatewayStatusText: String,
gatewayConnected: Bool,
agentName: String,
agentAvatarURL: String? = nil,
agentAvatarText: String? = nil,
sessionKey: String,
gatewayStableID: String? = nil,
talkStatusText: String,
talkEnabled: Bool,
talkListening: Bool,
talkSpeaking: Bool,
pendingApprovalCount: Int,
chatItems: [OpenClawWatchChatItem]? = nil,
chatStatusText: String? = nil,
sentAtMs: Int64? = nil,
snapshotId: String? = nil)
{
// Preserve the shipped source API while producers migrate to semantic statuses.
self.init(
gatewayStatus: Self.decodeLegacyGatewayStatus(
text: gatewayStatusText,
connected: gatewayConnected),
gatewayStatusText: gatewayStatusText,
gatewayConnected: gatewayConnected,
agentName: agentName,
agentAvatarURL: agentAvatarURL,
agentAvatarText: agentAvatarText,
sessionKey: sessionKey,
gatewayStableID: gatewayStableID,
talkStatus: Self.decodeLegacyTalkStatus(
text: talkStatusText,
enabled: talkEnabled,
listening: talkListening,
speaking: talkSpeaking),
talkStatusText: talkStatusText,
talkEnabled: talkEnabled,
talkListening: talkListening,
talkSpeaking: talkSpeaking,
pendingApprovalCount: pendingApprovalCount,
chatItems: chatItems,
chatStatus: Self.decodeLegacyChatStatus(code: nil, text: chatStatusText),
chatStatusText: chatStatusText,
sentAtMs: sentAtMs,
snapshotId: snapshotId)
}
private enum CodingKeys: String, CodingKey {
case type
case gatewayStatus
@@ -426,25 +473,25 @@ public struct OpenClawWatchAppSnapshotMessage: Codable, Sendable, Equatable {
self.snapshotId = try container.decodeIfPresent(String.self, forKey: .snapshotId)
let gatewayStatusText = try container.decodeIfPresent(String.self, forKey: .gatewayStatusText)
self.gatewayStatus = try container.decodeIfPresent(
self.gatewayStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .gatewayStatus) ?? Self.decodeLegacyGatewayStatus(
forKey: .gatewayStatus)) ?? Self.decodeLegacyGatewayStatus(
text: gatewayStatusText,
connected: self.gatewayConnected)
self.gatewayStatusText = gatewayStatusText ?? Self.legacyText(for: self.gatewayStatus)
let talkStatusText = try container.decodeIfPresent(String.self, forKey: .talkStatusText)
self.talkStatus = try container.decodeIfPresent(
self.talkStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .talkStatus) ?? Self.decodeLegacyTalkStatus(
forKey: .talkStatus)) ?? Self.decodeLegacyTalkStatus(
text: talkStatusText,
enabled: self.talkEnabled,
listening: self.talkListening,
speaking: self.talkSpeaking)
self.talkStatusText = talkStatusText ?? Self.legacyText(for: self.talkStatus)
let chatStatusText = try container.decodeIfPresent(String.self, forKey: .chatStatusText)
self.chatStatus = try container.decodeIfPresent(
self.chatStatus = (try? container.decode(
OpenClawWatchAppStatus.self,
forKey: .chatStatus) ?? Self.decodeLegacyChatStatus(
forKey: .chatStatus)) ?? Self.decodeLegacyChatStatus(
code: container.decodeIfPresent(String.self, forKey: .chatStatusCode),
text: chatStatusText)
self.chatStatusText = chatStatusText ?? self.chatStatus.map(Self.legacyText)

View File

@@ -29,4 +29,55 @@ struct WatchCommandsTests {
#expect(object["chatStatus"] != nil)
#expect(object["chatStatusText"] as? String == "No chat messages yet")
}
@Test func `shipped snapshot initializer remains available`() {
let message = OpenClawWatchAppSnapshotMessage(
gatewayStatusText: "Connected",
gatewayConnected: true,
agentName: "Main",
sessionKey: "main",
talkStatusText: "Off",
talkEnabled: false,
talkListening: false,
talkSpeaking: false,
pendingApprovalCount: 0)
#expect(message.gatewayStatus.code == .gatewayConnected)
#expect(message.talkStatus.code == .talkOff)
}
@Test func `unknown semantic statuses fall back to legacy text`() throws {
let json = """
{
"type": "watch.appSnapshot",
"gatewayStatus": {"code": "futureGateway", "arguments": []},
"gatewayStatusText": "Future gateway state",
"gatewayConnected": false,
"agentName": "Main",
"sessionKey": "main",
"talkStatus": {"code": "futureTalk", "arguments": []},
"talkStatusText": "Future Talk state",
"talkEnabled": true,
"talkListening": false,
"talkSpeaking": false,
"pendingApprovalCount": 0,
"chatStatus": {"code": "futureChat", "arguments": []},
"chatStatusText": "Future chat state"
}
"""
let message = try JSONDecoder().decode(
OpenClawWatchAppSnapshotMessage.self,
from: Data(json.utf8))
#expect(message.gatewayStatus == OpenClawWatchAppStatus(
code: .legacy,
verbatim: "Future gateway state"))
#expect(message.talkStatus == OpenClawWatchAppStatus(
code: .legacy,
verbatim: "Future Talk state"))
#expect(message.chatStatus == OpenClawWatchAppStatus(
code: .legacy,
verbatim: "Future chat state"))
}
}