openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatGatewayRequest.swift
Peter Steinberger 1128a4ae92
feat(ios): snooze sessions from the session menu (#162399)
* 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.
2026-10-01 10:22:22 +00:00

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: &params, key: "agentId")
self.add(sessionKey, to: &params, 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: &params, 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: &params,
key: "agentId")
if includeSessionKey {
self.add(sessionKey, to: &params, 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: &params, key: "agentId")
params["limit"] = limit.map(AnyCodable.init)
params["activeMinutes"] = activeMinutes.map(AnyCodable.init)
self.add(spawnedBy, to: &params, key: "spawnedBy")
params["offset"] = offset.map(AnyCodable.init)
params["configuredAgentsOnly"] = configuredAgentsOnly.map(AnyCodable.init)
self.add(search, to: &params, 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: &params, key: "agentId")
self.add(label, to: &params, key: "label", trim: false)
self.add(parentSessionKey, to: &params, key: "parentSessionKey", trim: false)
params["worktree"] = worktree.map(AnyCodable.init)
self.add(worktreeBaseRef, to: &params, 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: &params, 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: &params, 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: &params, 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: &params, 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: &params, 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: &params, 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: &params, 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: &params,
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: &params, key: "agentId")
self.add(
expectedSessionRoutingContract,
to: &params,
key: "expectedSessionRoutingContract")
self.add(thinking, to: &params, 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: &params, 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)
}
}