| import Foundation |
|
|
| |
| |
| |
| |
| |
| |
| public protocol OpenClawChatTranscriptCache: Sendable { |
| func loadSessions() async -> [OpenClawChatSessionEntry] |
| func loadSessions(agentID: String?) async -> [OpenClawChatSessionEntry] |
| func loadTranscript(sessionKey: String) async -> [OpenClawChatMessage] |
| func loadTranscript(sessionKey: String, agentID: String?) async -> [OpenClawChatMessage] |
| func storeSessions(_ sessions: [OpenClawChatSessionEntry]) async |
| func storeSessions(_ sessions: [OpenClawChatSessionEntry], agentID: String?) async |
| |
| |
| func storeCanonicalTranscript( |
| sessionKey: String, |
| agentID: String?, |
| messages: [OpenClawChatMessage], |
| canonicalMessageIdempotencyKeys: Set<String>) async |
| |
| |
| func observeCanonicalMessageIdempotencyKeys(_ keys: Set<String>) |
| } |
|
|
| extension OpenClawChatTranscriptCache { |
| public func loadSessions(agentID: String?) async -> [OpenClawChatSessionEntry] { |
| |
| |
| guard agentID == nil else { return [] } |
| return await self.loadSessions() |
| } |
|
|
| public func storeSessions(_ sessions: [OpenClawChatSessionEntry], agentID: String?) async { |
| guard agentID == nil else { return } |
| await self.storeSessions(sessions) |
| } |
|
|
| public func loadTranscript(sessionKey: String, agentID: String?) async -> [OpenClawChatMessage] { |
| guard agentID == nil else { return [] } |
| return await self.loadTranscript(sessionKey: sessionKey) |
| } |
|
|
| public func observeCanonicalMessageIdempotencyKeys(_: Set<String>) {} |
| } |
|
|
| |
| |
| |
| protocol OpenClawChatCanonicalTranscriptMerging: OpenClawChatTranscriptCache { |
| func mergeCanonicalTranscriptMessage( |
| sessionKey: String, |
| agentID: String?, |
| message: OpenClawChatMessage, |
| canonicalMessageIdempotencyKey: String) async |
| } |
|
|
| |
| public struct OpenClawChatOutboxScope: Hashable, Sendable { |
| public let sessionKey: String |
| public let agentID: String? |
|
|
| public init(sessionKey: String, agentID: String?) { |
| self.sessionKey = sessionKey |
| let normalizedAgentID = agentID?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() |
| self.agentID = normalizedAgentID?.isEmpty == false ? normalizedAgentID : nil |
| } |
| } |
|
|
| |
| public struct OpenClawChatOutboxBranchState: Equatable, Sendable { |
| public let epoch: Int |
| public let lastActiveLeafEntryID: String? |
| public let hadPendingCommands: Bool |
| public let switchPendingSince: TimeInterval? |
| public let needsReconciliation: Bool |
| public let revision: Int |
|
|
| public init( |
| epoch: Int, |
| lastActiveLeafEntryID: String?, |
| hadPendingCommands: Bool = false, |
| switchPendingSince: TimeInterval? = nil, |
| needsReconciliation: Bool = false, |
| revision: Int = 0) |
| { |
| self.epoch = epoch |
| self.lastActiveLeafEntryID = lastActiveLeafEntryID |
| self.hadPendingCommands = hadPendingCommands |
| self.switchPendingSince = switchPendingSince |
| self.needsReconciliation = needsReconciliation |
| self.revision = revision |
| } |
| } |
|
|
| public struct OpenClawChatOutboxRetryExpectation: Equatable, Sendable { |
| public let attemptVersion: Int |
| public let retryCount: Int |
| public let lastError: String? |
|
|
| public init(attemptVersion: Int, retryCount: Int, lastError: String?) { |
| self.attemptVersion = attemptVersion |
| self.retryCount = retryCount |
| self.lastError = lastError |
| } |
| } |
|
|
| |
| public struct OpenClawChatOutboxAttachment: Codable, Hashable, Sendable { |
| public let type: String |
| public let mimeType: String |
| public let fileName: String |
| public let data: Data |
| public let durationSeconds: Double? |
|
|
| public init( |
| type: String, |
| mimeType: String, |
| fileName: String, |
| data: Data, |
| durationSeconds: Double? = nil) |
| { |
| self.type = type |
| self.mimeType = mimeType |
| self.fileName = fileName |
| self.data = data |
| self.durationSeconds = durationSeconds |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public struct OpenClawChatOutboxCommand: Hashable, Sendable, Identifiable { |
| static let legacyUnboundRoutingContract = "legacy-unbound" |
|
|
| public enum Status: String, Sendable { |
| case queued |
| case sending |
| case awaitingConfirmation = "awaiting_confirmation" |
| case failed |
| } |
|
|
| public let id: String |
| |
| public let sessionKey: String |
| |
| |
| public let deliverySessionKey: String |
| |
| |
| public let routingContract: String? |
| |
| |
| public let agentID: String? |
| |
| public let branchEpoch: Int |
| |
| public let scopeBranchEpoch: Int? |
| public let text: String |
| |
| |
| public let attachments: [OpenClawChatOutboxAttachment] |
| |
| |
| public let thinking: String |
| |
| |
| public let expectedSessionSettings: OpenClawChatSessionSettingsExpectation? |
| |
| public let createdAt: Double |
| public var status: Status |
| |
| |
| public let attemptVersion: Int |
| public var retryCount: Int |
| public var lastError: String? |
|
|
| public init( |
| id: String, |
| sessionKey: String, |
| deliverySessionKey: String? = nil, |
| routingContract: String? = nil, |
| agentID: String? = nil, |
| branchEpoch: Int = 0, |
| scopeBranchEpoch: Int? = nil, |
| text: String, |
| attachments: [OpenClawChatOutboxAttachment] = [], |
| thinking: String, |
| expectedSessionSettings: OpenClawChatSessionSettingsExpectation? = nil, |
| createdAt: Double, |
| status: Status, |
| attemptVersion: Int = 1, |
| retryCount: Int, |
| lastError: String?) |
| { |
| self.id = id |
| self.sessionKey = sessionKey |
| if let deliverySessionKey { |
| self.deliverySessionKey = deliverySessionKey.trimmingCharacters(in: .whitespacesAndNewlines) |
| } else { |
| self.deliverySessionKey = sessionKey |
| } |
| let normalizedRoutingContract = routingContract?.trimmingCharacters(in: .whitespacesAndNewlines) |
| self.routingContract = normalizedRoutingContract?.isEmpty == false ? normalizedRoutingContract : nil |
| let normalizedAgentID = agentID?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() |
| self.agentID = normalizedAgentID?.isEmpty == false ? normalizedAgentID : nil |
| self.branchEpoch = branchEpoch |
| self.scopeBranchEpoch = scopeBranchEpoch ?? branchEpoch |
| self.text = text |
| self.attachments = attachments |
| self.thinking = thinking |
| self.expectedSessionSettings = expectedSessionSettings |
| self.createdAt = createdAt |
| self.status = status |
| self.attemptVersion = attemptVersion |
| self.retryCount = retryCount |
| self.lastError = lastError |
| } |
| } |
|
|
| public enum OpenClawChatOutboxUpdateResult: Equatable, Sendable { |
| case updated |
| case confirmed |
| case missing |
| case superseded |
| case unavailable |
| } |
|
|
| public enum OpenClawChatOutboxChange: Equatable, Sendable { |
| case canceled(gatewayID: String, id: String) |
| case confirmed(gatewayID: String, id: String) |
| case invalidated(gatewayID: String, scope: OpenClawChatOutboxScope) |
|
|
| var gatewayID: String { |
| switch self { |
| case let .canceled(gatewayID, _), let .confirmed(gatewayID, _), let .invalidated(gatewayID, _): |
| gatewayID |
| } |
| } |
| } |
|
|
| |
| |
| |
| public protocol OpenClawChatCommandOutbox: Sendable { |
| |
| |
| func enqueueCommand(_ command: OpenClawChatOutboxCommand) async -> Bool |
| |
| |
| |
| func loadCommands() async -> [OpenClawChatOutboxCommand] |
| |
| |
| func loadCommandsIfAvailable() async -> [OpenClawChatOutboxCommand]? |
| |
| |
| |
| |
| |
| @discardableResult |
| func recoverInterruptedSends() async -> Bool |
| |
| |
| func claimNextCommand() async -> OpenClawChatOutboxCommand? |
| |
| |
| func markCommandQueued( |
| id: String, |
| attemptVersion: Int, |
| retryCount: Int, |
| lastError: String?) async -> OpenClawChatOutboxUpdateResult |
| func markCommandAwaitingConfirmation( |
| id: String, |
| attemptVersion: Int) async -> OpenClawChatOutboxUpdateResult |
| |
| |
| func markCommandFailedIfPresent( |
| id: String, |
| attemptVersion: Int, |
| retryCount: Int, |
| lastError: String?) async -> OpenClawChatOutboxUpdateResult |
| |
| func branchState(for scope: OpenClawChatOutboxScope) async -> OpenClawChatOutboxBranchState? |
| |
| |
| func beginBranchSwitch(_ scope: OpenClawChatOutboxScope) async -> Bool |
| |
| func cancelBranchSwitch(_ scope: OpenClawChatOutboxScope) async -> Bool |
| |
| |
| func demoteBranchSwitchToReconcile(_ scope: OpenClawChatOutboxScope) async -> Bool |
| |
| |
| func reconcileBranchScope( |
| _ scope: OpenClawChatOutboxScope, |
| previousState: OpenClawChatOutboxBranchState, |
| activeLeafEntryID: String?, |
| branchLeafEntryIDs: Set<String>, |
| activeTranscriptEntryIDs: Set<String>, |
| lastError: String) async -> [OpenClawChatOutboxCommand]? |
| |
| |
| func confirmBranchChange( |
| _ scope: OpenClawChatOutboxScope, |
| activeLeafEntryID: String, |
| lastError: String) async -> [OpenClawChatOutboxCommand]? |
| |
| |
| func updateLastActiveLeafEntryID( |
| _ leafEntryID: String, |
| expectedEpoch: Int, |
| for scope: OpenClawChatOutboxScope) async -> Bool |
| |
| |
| func markCommandRetriedIfPresent( |
| id: String, |
| expectation: OpenClawChatOutboxRetryExpectation, |
| agentID: String?, |
| deliverySessionKey: String, |
| routingContract: String, |
| expectedSessionSettings: OpenClawChatSessionSettingsExpectation, |
| replacementID: String?) async -> OpenClawChatOutboxUpdateResult |
| |
| func parkQueuedCommands( |
| in scope: OpenClawChatOutboxScope, |
| lastError: String) async -> Bool |
| |
| |
| func cancelCommand(id: String) async -> OpenClawChatOutboxUpdateResult |
| |
| |
| func confirmCommand(id: String, attemptVersion: Int) async -> OpenClawChatOutboxUpdateResult |
| |
| func changes() -> AsyncStream<OpenClawChatOutboxChange> |
| } |
|
|
| extension OpenClawChatCommandOutbox { |
| public func parkQueuedCommands( |
| in _: OpenClawChatOutboxScope, |
| lastError _: String) async -> Bool |
| { |
| false |
| } |
|
|
| public func markCommandQueued( |
| id _: String, |
| attemptVersion _: Int, |
| retryCount _: Int, |
| lastError _: String?) async -> OpenClawChatOutboxUpdateResult |
| { |
| .unavailable |
| } |
|
|
| public func markCommandAwaitingConfirmation( |
| id _: String, |
| attemptVersion _: Int) async -> OpenClawChatOutboxUpdateResult |
| { |
| .unavailable |
| } |
|
|
| public func markCommandFailedIfPresent( |
| id _: String, |
| attemptVersion _: Int, |
| retryCount _: Int, |
| lastError _: String?) async -> OpenClawChatOutboxUpdateResult |
| { |
| .unavailable |
| } |
|
|
| public func confirmCommand( |
| id _: String, |
| attemptVersion _: Int) async -> OpenClawChatOutboxUpdateResult |
| { |
| .unavailable |
| } |
|
|
| public func branchState(for _: OpenClawChatOutboxScope) async -> OpenClawChatOutboxBranchState? { |
| nil |
| } |
|
|
| public func beginBranchSwitch(_: OpenClawChatOutboxScope) async -> Bool { |
| false |
| } |
|
|
| public func cancelBranchSwitch(_: OpenClawChatOutboxScope) async -> Bool { |
| false |
| } |
|
|
| public func demoteBranchSwitchToReconcile(_: OpenClawChatOutboxScope) async -> Bool { |
| false |
| } |
|
|
| public func reconcileBranchScope( |
| _: OpenClawChatOutboxScope, |
| previousState _: OpenClawChatOutboxBranchState, |
| activeLeafEntryID _: String?, |
| branchLeafEntryIDs _: Set<String>, |
| activeTranscriptEntryIDs _: Set<String>, |
| lastError _: String) async -> [OpenClawChatOutboxCommand]? |
| { |
| nil |
| } |
|
|
| public func confirmBranchChange( |
| _: OpenClawChatOutboxScope, |
| activeLeafEntryID _: String, |
| lastError _: String) async -> [OpenClawChatOutboxCommand]? |
| { |
| nil |
| } |
|
|
| public func updateLastActiveLeafEntryID( |
| _: String, |
| expectedEpoch _: Int, |
| for _: OpenClawChatOutboxScope) async -> Bool |
| { |
| false |
| } |
|
|
| |
| public func markCommandRetriedIfPresent( |
| id _: String, |
| expectation _: OpenClawChatOutboxRetryExpectation, |
| agentID _: String?, |
| deliverySessionKey _: String, |
| routingContract _: String, |
| expectedSessionSettings _: OpenClawChatSessionSettingsExpectation, |
| replacementID _: String? = nil) async -> OpenClawChatOutboxUpdateResult |
| { |
| .unavailable |
| } |
| } |
|
|
| public struct OpenClawChatSessionRoutingIdentity: Equatable, Sendable { |
| public let scope: String |
| public let mainSessionKey: String |
| public let defaultAgentID: String |
| public let contract: String |
|
|
| public init?(contract: String?) { |
| guard let components = OpenClawChatSessionRoutingContract.parse(contract) else { return nil } |
| self.scope = components.scope |
| self.mainSessionKey = components.mainKey |
| self.defaultAgentID = components.defaultAgentID |
| self.contract = "\(components.scope)|\(components.mainKey)|\(components.defaultAgentID)" |
| } |
|
|
| public init?(scope: String?, mainSessionKey: String?, defaultAgentID: String?) { |
| guard let contract = OpenClawChatSessionRoutingContract.make( |
| scope: scope, |
| mainKey: mainSessionKey, |
| defaultAgentID: defaultAgentID) |
| else { return nil } |
| self.init(contract: contract) |
| } |
| } |
|
|