mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* feat(ios): snooze sessions from the session menu Add shared calendar presets and identity-bound snooze/wake patches while preserving existing Swift transport call sites. iOS gains Active, Snoozed, and Archived scopes, wake labels, and deadline-driven updates for Sessions, the sidebar, and Overview. Snoozing keeps ongoing work and the open conversation intact. * test(ios): capture session snooze proof states Extend the synthetic Gateway with awake and snoozed sessions. Capture Active and Snoozed lists, snooze presets, and Wake through the real iOS session menu, using All Sessions navigation and a row selector that excludes the retained sidebar. * fix(ios): show the weekday for the next-week snooze preset * chore(ios): record snooze strings in the native i18n source inventory * fix(ios): format snooze wake labels through the localized catalog Apple i18n verification rejects runtime-interpolated localized strings because they bypass the generated catalog, so the "Wake session" menu item and the "tomorrow" wake description now use the repository's String(format: String(localized: "… %@")) pattern, and the native string inventory records the format sources. * refactor(ios): drop the superseded mutation lease initializer * fix(ios): keep snoozed sessions browsable offline Separate cached browsing scopes from connected mutation controls. Keep Archived connected-only and reset an archived selection to Active on disconnect. Cover scope availability and the offline cached roster, including snoozed rows and wake labels.
806 lines
30 KiB
Swift
806 lines
30 KiB
Swift
import Foundation
|
|
import OpenClawKit
|
|
import OpenClawProtocol
|
|
|
|
public struct OpenClawChatGatewayRequest: Sendable, Equatable {
|
|
public let method: String
|
|
public let params: [String: AnyCodable]
|
|
public let timeoutMs: Double
|
|
|
|
public init(method: String, params: [String: AnyCodable] = [:], timeoutMs: Double) {
|
|
self.method = method
|
|
self.params = params
|
|
self.timeoutMs = timeoutMs
|
|
}
|
|
}
|
|
|
|
public enum OpenClawChatSessionUnreadPatch: Sendable, Equatable {
|
|
case markUnread
|
|
case read
|
|
case automaticRead(expectedMarkedUnreadAt: Double?)
|
|
|
|
public static func routed(
|
|
unread: Bool?,
|
|
expectedMarkedUnreadAt: Double??,
|
|
supportsReadContract: Bool) -> Self?
|
|
{
|
|
guard let unread else { return nil }
|
|
guard !unread else { return .markUnread }
|
|
guard supportsReadContract else { return .read }
|
|
if let expectedMarkedUnreadAt {
|
|
return .automaticRead(expectedMarkedUnreadAt: expectedMarkedUnreadAt)
|
|
}
|
|
return .read
|
|
}
|
|
}
|
|
|
|
public enum OpenClawChatSessionTargetPolicy: Sendable {
|
|
case preserveBareKeys
|
|
case scopeBareKeysToSelectedAgent
|
|
}
|
|
|
|
public struct OpenClawChatSessionTarget: Sendable, Hashable {
|
|
public let sessionKey: String
|
|
public let agentID: String?
|
|
|
|
public init(sessionKey: String, agentID: String?) {
|
|
self.sessionKey = sessionKey
|
|
self.agentID = agentID
|
|
}
|
|
|
|
public static func resolve(
|
|
_ rawSessionKey: String,
|
|
selectedAgentID: String?,
|
|
overrideAgentID: String? = nil,
|
|
policy: OpenClawChatSessionTargetPolicy) -> Self
|
|
{
|
|
let sessionKey = rawSessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let selected = self.normalizedAgentID(selectedAgentID)
|
|
let override = self.normalizedAgentID(overrideAgentID)
|
|
|
|
if OpenClawChatSessionKey.agentID(from: sessionKey) != nil {
|
|
return Self(sessionKey: sessionKey, agentID: override)
|
|
}
|
|
let lowercasedKey = sessionKey.lowercased()
|
|
if lowercasedKey.hasPrefix("agent:") || lowercasedKey == "unknown" {
|
|
return Self(sessionKey: sessionKey, agentID: nil)
|
|
}
|
|
if lowercasedKey == "global" {
|
|
return Self(sessionKey: sessionKey, agentID: override ?? selected)
|
|
}
|
|
|
|
switch policy {
|
|
case .preserveBareKeys:
|
|
return Self(sessionKey: sessionKey, agentID: override)
|
|
case .scopeBareKeysToSelectedAgent:
|
|
guard let agentID = override ?? selected else {
|
|
return Self(sessionKey: sessionKey, agentID: nil)
|
|
}
|
|
return Self(sessionKey: "agent:\(agentID):\(sessionKey)", agentID: nil)
|
|
}
|
|
}
|
|
|
|
private static func normalizedAgentID(_ agentID: String?) -> String? {
|
|
let normalized = agentID?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return normalized?.isEmpty == false ? normalized : nil
|
|
}
|
|
}
|
|
|
|
public enum OpenClawChatGatewayRequests {
|
|
private static let defaultTimeoutMs: Double = 15000
|
|
private static let mutationTimeoutMs: Double = 15000
|
|
private static let archiveMutationTimeoutMs: Double = 10 * 60 * 1000
|
|
private static let shortTimeoutMs: Double = 10000
|
|
private static let compactionTimeoutMs: Double = 0
|
|
|
|
public static func agentsList(timeoutMs: Double = 15000) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(method: "agents.list", timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func agentIdentity(agentID: String) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "agent.identity.get",
|
|
params: ["agentId": AnyCodable(agentID.trimmingCharacters(in: .whitespacesAndNewlines))],
|
|
timeoutMs: self.shortTimeoutMs)
|
|
}
|
|
|
|
public static func modelsList(agentID: String?, sessionKey: String? = nil) -> OpenClawChatGatewayRequest {
|
|
var params: [String: AnyCodable] = [:]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
self.add(sessionKey, to: ¶ms, key: "sessionKey")
|
|
if sessionKey != nil {
|
|
params["view"] = AnyCodable("configured")
|
|
params["includeDetails"] = AnyCodable(true)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "models.list",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func composerSkillsStatus(agentID: String?) -> OpenClawChatGatewayRequest {
|
|
var params: [String: AnyCodable] = [:]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "skills.status",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func composerConfigGet() -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(method: "config.get", timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func composerToolsEffective(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "tools.effective",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey"),
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func artifactDownload(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
artifactId: String) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey")
|
|
params["artifactId"] = AnyCodable(artifactId)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "artifacts.download",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func chatMetadata(
|
|
sessionKey: String,
|
|
fallbackAgentID: String?,
|
|
includeSessionKey: Bool = false) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [:]
|
|
self.add(
|
|
OpenClawChatSessionKey.agentID(from: sessionKey) ?? fallbackAgentID,
|
|
to: ¶ms,
|
|
key: "agentId")
|
|
if includeSessionKey {
|
|
self.add(sessionKey, to: ¶ms, key: "sessionKey")
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.metadata",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func questionList() -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(method: "question.list", timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func questionGet(id: String) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "question.get",
|
|
params: ["id": AnyCodable(id)],
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func resolveQuestion(
|
|
id: String,
|
|
answers: [String: [String]],
|
|
secretStoreAllowedHosts: [String]? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"id": AnyCodable(id),
|
|
"answers": AnyCodable(["answers": answers]),
|
|
]
|
|
params["secretStoreAllowedHosts"] = secretStoreAllowedHosts.map(AnyCodable.init)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "question.resolve",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func cancelQuestion(id: String) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "question.resolve",
|
|
params: [
|
|
"id": AnyCodable(id),
|
|
"cancel": AnyCodable(true),
|
|
],
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func sessionsList(
|
|
limit: Int?,
|
|
search: String?,
|
|
archived: Bool,
|
|
agentID: String? = nil,
|
|
includeGlobal: Bool = true,
|
|
includeUnknown: Bool = false,
|
|
activeMinutes: Int? = nil,
|
|
spawnedBy: String? = nil,
|
|
offset: Int? = nil,
|
|
configuredAgentsOnly: Bool? = nil,
|
|
timeoutMs: Double = 15000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"includeGlobal": AnyCodable(includeGlobal),
|
|
"includeUnknown": AnyCodable(includeUnknown),
|
|
]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
params["limit"] = limit.map(AnyCodable.init)
|
|
params["activeMinutes"] = activeMinutes.map(AnyCodable.init)
|
|
self.add(spawnedBy, to: ¶ms, key: "spawnedBy")
|
|
params["offset"] = offset.map(AnyCodable.init)
|
|
params["configuredAgentsOnly"] = configuredAgentsOnly.map(AnyCodable.init)
|
|
self.add(search, to: ¶ms, key: "search")
|
|
if archived {
|
|
params["archived"] = AnyCodable(true)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.list",
|
|
params: params,
|
|
timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func sessionGroupsList() -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.groups.list",
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func sessionGroupsPut(names: [String]) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.groups.put",
|
|
params: ["names": AnyCodable(names)],
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func sessionGroupsRename(
|
|
name: String,
|
|
to: String) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.groups.rename",
|
|
params: [
|
|
"name": AnyCodable(name),
|
|
"to": AnyCodable(to),
|
|
],
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func sessionGroupsDelete(name: String) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.groups.delete",
|
|
params: ["name": AnyCodable(name)],
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func createSession(
|
|
key: String,
|
|
agentID: String?,
|
|
label: String?,
|
|
parentSessionKey: String?,
|
|
worktree: Bool?,
|
|
worktreeBaseRef: String? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = ["key": AnyCodable(key)]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
self.add(label, to: ¶ms, key: "label", trim: false)
|
|
self.add(parentSessionKey, to: ¶ms, key: "parentSessionKey", trim: false)
|
|
params["worktree"] = worktree.map(AnyCodable.init)
|
|
self.add(worktreeBaseRef, to: ¶ms, key: "worktreeBaseRef")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.create",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func abortRun(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
runID: String,
|
|
requestTimeoutMs: Int = 10000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey")
|
|
params["runId"] = AnyCodable(runID)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.abort",
|
|
params: params,
|
|
timeoutMs: Double(requestTimeoutMs))
|
|
}
|
|
|
|
public static func patchSessionPreferences(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
thinkingLevel: String?? = nil,
|
|
fastMode: OpenClawChatFastMode?? = nil,
|
|
verboseLevel: String?? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
self.patchSessionSettings(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
thinkingLevel: thinkingLevel,
|
|
fastMode: fastMode,
|
|
verboseLevel: verboseLevel)
|
|
}
|
|
|
|
public static func patchSessionSettings(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
patch: OpenClawChatSessionSettingsPatch,
|
|
supportsSessionSettingsContract: Bool,
|
|
supportsSessionSettingsCAS: Bool) throws -> OpenClawChatGatewayRequest
|
|
{
|
|
guard !patch.requiresSessionSettingsContract || supportsSessionSettingsContract,
|
|
!patch.requiresSessionSettingsCAS || supportsSessionSettingsCAS
|
|
else { throw OpenClawChatTransportSendError.notDispatched }
|
|
return self.patchSessionSettings(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
expectedSessionID: patch.expectedSessionID,
|
|
expectedPermissionMode: patch.expectedPermissionMode,
|
|
expectedToolOverrides: patch.expectedToolOverrides,
|
|
model: patch.model,
|
|
thinkingLevel: patch.thinkingLevel,
|
|
fastMode: patch.fastMode,
|
|
verboseLevel: patch.verboseLevel,
|
|
permissionMode: patch.permissionMode,
|
|
toolOverrides: patch.toolOverrides,
|
|
supportsSessionSettingsContract: supportsSessionSettingsContract,
|
|
supportsSessionSettingsCAS: supportsSessionSettingsCAS)
|
|
}
|
|
|
|
public static func patchSessionSettings(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
expectedSessionID: String? = nil,
|
|
expectedPermissionMode: OpenClawChatPermissionMode?? = nil,
|
|
expectedToolOverrides: OpenClawChatSessionToolOverrides?? = nil,
|
|
model: String?? = nil,
|
|
thinkingLevel: String?? = nil,
|
|
fastMode: OpenClawChatFastMode?? = nil,
|
|
verboseLevel: String?? = nil,
|
|
permissionMode: OpenClawChatPermissionMode?? = nil,
|
|
toolOverrides: OpenClawChatSessionToolOverrides?? = nil,
|
|
supportsSessionSettingsContract: Bool = false,
|
|
supportsSessionSettingsCAS: Bool = false) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
if let model {
|
|
params["model"] = model.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let thinkingLevel {
|
|
params["thinkingLevel"] = thinkingLevel.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let fastMode {
|
|
params["fastMode"] = fastMode.map(self.fastModeValue) ?? AnyCodable(NSNull())
|
|
}
|
|
if let verboseLevel {
|
|
params["verboseLevel"] = verboseLevel.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if supportsSessionSettingsContract {
|
|
self.add(expectedSessionID, to: ¶ms, key: "expectedSessionId")
|
|
if let permissionMode {
|
|
params["permissionMode"] = permissionMode.map { AnyCodable($0.rawValue) } ?? AnyCodable(NSNull())
|
|
}
|
|
if let toolOverrides {
|
|
params["toolOverrides"] = toolOverrides.map(self.toolOverridesValue) ?? AnyCodable(NSNull())
|
|
}
|
|
}
|
|
if supportsSessionSettingsCAS, let expectedToolOverrides {
|
|
params["expectedToolOverrides"] = expectedToolOverrides
|
|
.map(self.toolOverridesValue) ?? AnyCodable(NSNull())
|
|
}
|
|
if supportsSessionSettingsCAS, let expectedPermissionMode {
|
|
params["expectedPermissionMode"] = expectedPermissionMode
|
|
.map { AnyCodable($0.rawValue) } ?? AnyCodable(NSNull())
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.patch",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
private static func fastModeValue(_ mode: OpenClawChatFastMode) -> AnyCodable {
|
|
switch mode {
|
|
case .off: AnyCodable(false)
|
|
case .on: AnyCodable(true)
|
|
case .automatic: AnyCodable("auto")
|
|
case .ultrafast: AnyCodable("ultrafast")
|
|
}
|
|
}
|
|
|
|
private static func toolOverridesValue(_ overrides: OpenClawChatSessionToolOverrides) -> AnyCodable {
|
|
var value: [String: AnyCodable] = [:]
|
|
if let webSearch = overrides.webSearch {
|
|
value["webSearch"] = AnyCodable(webSearch)
|
|
}
|
|
if !overrides.skills.isEmpty {
|
|
value["skills"] = AnyCodable(overrides.skills.mapValues(AnyCodable.init))
|
|
}
|
|
if !overrides.mcpServers.isEmpty {
|
|
value["mcpServers"] = AnyCodable(overrides.mcpServers.mapValues(AnyCodable.init))
|
|
}
|
|
if !overrides.mcpToolsDeny.isEmpty {
|
|
value["mcpToolsDeny"] = AnyCodable(overrides.mcpToolsDeny.mapValues { AnyCodable($0) })
|
|
}
|
|
return AnyCodable(value)
|
|
}
|
|
|
|
public static func patchSession(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
expectedSessionID: String? = nil,
|
|
label: String??,
|
|
category: String??,
|
|
color: String?? = nil,
|
|
pinned: Bool?,
|
|
archived: Bool?,
|
|
snoozedUntil: OpenClawChatSnoozePatch? = nil,
|
|
unreadPatch: OpenClawChatSessionUnreadPatch?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
self.add(expectedSessionID, to: ¶ms, key: "expectedSessionId")
|
|
if let label {
|
|
params["label"] = label.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let category {
|
|
params["category"] = category.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
if let color {
|
|
params["color"] = color.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
}
|
|
params["pinned"] = pinned.map(AnyCodable.init)
|
|
params["archived"] = archived.map(AnyCodable.init)
|
|
if let snoozedUntil {
|
|
switch snoozedUntil {
|
|
case let .until(wakeAt):
|
|
params["snoozedUntil"] = AnyCodable(Int(wakeAt.timeIntervalSince1970 * 1000))
|
|
case .wake:
|
|
params["snoozedUntil"] = AnyCodable(NSNull())
|
|
}
|
|
}
|
|
switch unreadPatch {
|
|
case .markUnread:
|
|
params["unread"] = AnyCodable(true)
|
|
case .read:
|
|
params["unread"] = AnyCodable(false)
|
|
case let .automaticRead(expectedMarkedUnreadAt):
|
|
params["unread"] = AnyCodable(false)
|
|
params["expectedMarkedUnreadAt"] = expectedMarkedUnreadAt.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
|
case nil:
|
|
break
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.patch",
|
|
params: params,
|
|
timeoutMs: archived == true ? self.archiveMutationTimeoutMs : self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func deleteSession(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
params["deleteTranscript"] = AnyCodable(true)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.delete",
|
|
params: params,
|
|
timeoutMs: self.archiveMutationTimeoutMs)
|
|
}
|
|
|
|
public static func forkSession(
|
|
parentSessionKey: String,
|
|
agentID: String?,
|
|
fromLastCompleted: Bool = false) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"parentSessionKey": AnyCodable(parentSessionKey),
|
|
"fork": AnyCodable(true),
|
|
]
|
|
if fromLastCompleted {
|
|
params["forkFrom"] = AnyCodable("last-completed")
|
|
}
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.create",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func rewindSession(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
entryId: String) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
key: "sessionKey")
|
|
self.add(entryId, to: ¶ms, key: "entryId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.rewind",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func forkAtMessage(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
entryId: String) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
key: "sessionKey")
|
|
self.add(entryId, to: ¶ms, key: "entryId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.fork",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func listSessionBranches(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
let params = self.sessionParams(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
key: "sessionKey")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.branches.list",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func switchSessionBranch(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
leafEntryId: String) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(
|
|
sessionKey: sessionKey,
|
|
agentID: agentID,
|
|
key: "sessionKey")
|
|
self.add(leafEntryId, to: ¶ms, key: "leafEntryId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.branches.switch",
|
|
params: params,
|
|
timeoutMs: self.mutationTimeoutMs)
|
|
}
|
|
|
|
public static func subscribeSessions(timeoutMs: Double = 10000) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.subscribe",
|
|
timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func setSessionObserverVisibility(
|
|
_ visible: Bool,
|
|
timeoutMs: Double = 10000) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.observer.visibility",
|
|
params: ["visible": AnyCodable(visible)],
|
|
timeoutMs: timeoutMs)
|
|
}
|
|
|
|
public static func subscribeSessionMessages(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.messages.subscribe",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID),
|
|
timeoutMs: self.shortTimeoutMs)
|
|
}
|
|
|
|
public static func resetSession(
|
|
sessionKey: String,
|
|
agentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "sessions.reset",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID),
|
|
timeoutMs: self.shortTimeoutMs)
|
|
}
|
|
|
|
public static func compactSession(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
maxLines: Int? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID)
|
|
params["maxLines"] = maxLines.map(AnyCodable.init)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "sessions.compact",
|
|
params: params,
|
|
timeoutMs: self.compactionTimeoutMs)
|
|
}
|
|
|
|
public static func history(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
limit: Int? = nil,
|
|
maxChars: Int? = nil,
|
|
inputRunIDs: [String]? = nil,
|
|
timeoutMs: Int? = nil) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey")
|
|
params["limit"] = limit.map(AnyCodable.init)
|
|
params["maxChars"] = maxChars.map(AnyCodable.init)
|
|
if let inputRunIDs, !inputRunIDs.isEmpty {
|
|
params["inputRunIds"] = AnyCodable(inputRunIDs)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.history",
|
|
params: params,
|
|
timeoutMs: timeoutMs.map(Double.init) ?? self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func reactionsList(sessionKey: String, agentID: String?) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "session.reactions.list",
|
|
params: self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey"),
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func reactionsSet(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
messageID: String,
|
|
emoji: String,
|
|
remove: Bool) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params = self.sessionParams(sessionKey: sessionKey, agentID: agentID, key: "sessionKey")
|
|
params["messageId"] = AnyCodable(messageID)
|
|
params["emoji"] = AnyCodable(emoji)
|
|
params["remove"] = AnyCodable(remove)
|
|
return OpenClawChatGatewayRequest(
|
|
method: "session.reactions.set",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func progressCardGet(sessionKey: String, agentID: String?) -> OpenClawChatGatewayRequest {
|
|
let target = OpenClawChatSessionTarget.resolve(
|
|
sessionKey,
|
|
selectedAgentID: nil,
|
|
overrideAgentID: agentID,
|
|
policy: .scopeBareKeysToSelectedAgent)
|
|
var params: [String: AnyCodable] = ["sessionKey": AnyCodable(target.sessionKey)]
|
|
// Released gateways reject extra fields; qualified keys already carry their owner.
|
|
if target.agentID != OpenClawChatSessionKey.agentID(from: target.sessionKey)?.lowercased() {
|
|
self.add(target.agentID, to: ¶ms, key: "agentId")
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "progressCard.get",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func commandsList(
|
|
sessionKey: String?,
|
|
fallbackAgentID: String?) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"scope": AnyCodable("text"),
|
|
"includeArgs": AnyCodable(true),
|
|
]
|
|
self.add(
|
|
sessionKey.flatMap(OpenClawChatSessionKey.agentID) ?? fallbackAgentID,
|
|
to: ¶ms,
|
|
key: "agentId")
|
|
return OpenClawChatGatewayRequest(
|
|
method: "commands.list",
|
|
params: params,
|
|
timeoutMs: self.defaultTimeoutMs)
|
|
}
|
|
|
|
public static func sendMessage(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
expectedSessionRoutingContract: String?,
|
|
expectedSessionSettings: OpenClawChatSessionSettingsExpectation? = nil,
|
|
supportsSessionSettingsCAS: Bool = false,
|
|
message: String,
|
|
thinking: String?,
|
|
idempotencyKey: String,
|
|
attachments: [OpenClawChatAttachmentPayload],
|
|
runTimeoutMs: Int? = nil,
|
|
requestTimeoutMs: Int = 30000) -> OpenClawChatGatewayRequest
|
|
{
|
|
var params: [String: AnyCodable] = [
|
|
"sessionKey": AnyCodable(sessionKey),
|
|
"message": AnyCodable(message),
|
|
"idempotencyKey": AnyCodable(idempotencyKey),
|
|
]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
self.add(
|
|
expectedSessionRoutingContract,
|
|
to: ¶ms,
|
|
key: "expectedSessionRoutingContract")
|
|
self.add(thinking, to: ¶ms, key: "thinking")
|
|
if supportsSessionSettingsCAS, let expectedSessionSettings {
|
|
params["expectedPermissionMode"] = expectedSessionSettings.permissionMode
|
|
.map { AnyCodable($0.rawValue) } ?? AnyCodable(NSNull())
|
|
params["expectedToolOverrides"] = expectedSessionSettings.toolOverrides
|
|
.map(self.toolOverridesValue) ?? AnyCodable(NSNull())
|
|
}
|
|
params["timeoutMs"] = runTimeoutMs.map(AnyCodable.init)
|
|
if !attachments.isEmpty {
|
|
let encoded = attachments.map { attachment in
|
|
[
|
|
"type": attachment.type,
|
|
"mimeType": attachment.mimeType,
|
|
"fileName": attachment.fileName,
|
|
"content": attachment.content,
|
|
]
|
|
}
|
|
params["attachments"] = AnyCodable(encoded)
|
|
}
|
|
return OpenClawChatGatewayRequest(
|
|
method: "chat.send",
|
|
params: params,
|
|
timeoutMs: Double(requestTimeoutMs))
|
|
}
|
|
|
|
public static func agentWait(
|
|
runID: String,
|
|
timeoutMs: Int,
|
|
requestGraceMs: Int = 5000) -> OpenClawChatGatewayRequest
|
|
{
|
|
OpenClawChatGatewayRequest(
|
|
method: "agent.wait",
|
|
params: [
|
|
"runId": AnyCodable(runID),
|
|
"timeoutMs": AnyCodable(timeoutMs),
|
|
],
|
|
timeoutMs: Double(timeoutMs + requestGraceMs))
|
|
}
|
|
|
|
public static func health(timeoutMs: Int) -> OpenClawChatGatewayRequest {
|
|
OpenClawChatGatewayRequest(
|
|
method: "health",
|
|
timeoutMs: Double(max(1, timeoutMs)))
|
|
}
|
|
|
|
private static func sessionParams(
|
|
sessionKey: String,
|
|
agentID: String?,
|
|
key: String = "key") -> [String: AnyCodable]
|
|
{
|
|
var params = [key: AnyCodable(sessionKey)]
|
|
self.add(agentID, to: ¶ms, key: "agentId")
|
|
return params
|
|
}
|
|
|
|
private static func add(
|
|
_ value: String?,
|
|
to params: inout [String: AnyCodable],
|
|
key: String,
|
|
trim: Bool = true)
|
|
{
|
|
let value = trim ? ChatPayloadDecoding.trimmedNonEmptyString(value) : value
|
|
if let value {
|
|
params[key] = AnyCodable(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GatewayNodeSession {
|
|
public func request(
|
|
_ request: OpenClawChatGatewayRequest,
|
|
ifCurrentRoute expectedRoute: GatewayNodeSessionRoute? = nil,
|
|
distinguishPreDispatchRouteChange: Bool = false) async throws -> Data
|
|
{
|
|
try await self.request(
|
|
method: request.method,
|
|
params: request.params,
|
|
timeoutMs: request.timeoutMs,
|
|
ifCurrentRoute: expectedRoute,
|
|
distinguishPreDispatchRouteChange: distinguishPreDispatchRouteChange)
|
|
}
|
|
}
|