Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/ios/Sources/Chat/IOSGatewayChatTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import OSLog
struct IOSGatewayChatTransport: OpenClawChatTransport {
static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "ios.chat.transport")
static let defaultChatSendTimeoutMs = 30000
static let compactionRequestTimeoutSeconds = 0
private let gateway: GatewayNodeSession

private struct CreateSessionParams: Codable {
Expand Down Expand Up @@ -205,7 +206,11 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {

func compactSession(sessionKey: String) async throws {
let json = try Self.makeSessionKeyParamsJSON(sessionKey)
_ = try await self.gateway.request(method: "sessions.compact", paramsJSON: json, timeoutSeconds: 10)
let response = try await self.gateway.request(
method: "sessions.compact",
paramsJSON: json,
timeoutSeconds: Self.compactionRequestTimeoutSeconds)
try OpenClawSessionsCompactResponse.requireSuccess(from: response)
}

func requestHistory(sessionKey: String) async throws -> OpenClawChatHistoryPayload {
Expand Down
4 changes: 4 additions & 0 deletions apps/ios/Tests/IOSGatewayChatTransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import Testing
#expect(IOSGatewayChatTransport.agentWaitRequestTimeoutSeconds(timeoutMs: 30000) == 35)
}

@Test func compactionLeavesTerminalTimeoutToGateway() {
#expect(IOSGatewayChatTransport.compactionRequestTimeoutSeconds == 0)
}

@Test func agentWaitCompletionDecodesFallbackRunId() throws {
let data = Data(#"{"status":"completed"}"#.utf8)
let completion = try IOSGatewayChatTransport.decodeAgentWaitCompletion(data, fallbackRunId: "run-local")
Expand Down
6 changes: 4 additions & 2 deletions apps/macos/Sources/OpenClaw/ControlChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ final class ControlChannel {
func request(
method: String,
params: [String: AnyHashable]? = nil,
timeoutMs: Double? = nil) async throws -> Data
timeoutMs: Double? = nil,
retryTransportFailures: Bool = true) async throws -> Data
{
do {
let rawParams = params?.reduce(into: [String: OpenClawKit.AnyCodable]()) {
Expand All @@ -254,7 +255,8 @@ final class ControlChannel {
let data = try await GatewayConnection.shared.request(
method: method,
params: rawParams,
timeoutMs: timeoutMs)
timeoutMs: timeoutMs,
retryTransportFailures: retryTransportFailures)
self.setStateThrottled(.connected)
return data
} catch {
Expand Down
5 changes: 3 additions & 2 deletions apps/macos/Sources/OpenClaw/GatewayConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ actor GatewayConnection {
func request(
method: String,
params: [String: AnyCodable]?,
timeoutMs: Double? = nil) async throws -> Data
timeoutMs: Double? = nil,
retryTransportFailures: Bool = true) async throws -> Data
{
let cfg = try await self.configProvider()
await self.configure(url: cfg.url, token: cfg.token, password: cfg.password)
Expand All @@ -174,7 +175,7 @@ actor GatewayConnection {
do {
return try await client.request(method: method, params: params, timeoutMs: timeoutMs)
} catch {
if error is GatewayResponseError || error is GatewayDecodingError {
if !retryTransportFailures || error is GatewayResponseError || error is GatewayDecodingError {
throw error
}

Expand Down
8 changes: 6 additions & 2 deletions apps/macos/Sources/OpenClaw/SessionActions.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import Foundation
import OpenClawKit

enum SessionActions {
static func patchSession(
Expand Down Expand Up @@ -32,9 +33,12 @@ enum SessionActions {
}

static func compactSession(key: String, maxLines: Int = 400) async throws {
_ = try await ControlChannel.shared.request(
let response = try await ControlChannel.shared.request(
method: "sessions.compact",
params: ["key": AnyHashable(key), "maxLines": AnyHashable(maxLines)])
params: ["key": AnyHashable(key), "maxLines": AnyHashable(maxLines)],
timeoutMs: 0,
retryTransportFailures: false)
try OpenClawSessionsCompactResponse.requireSuccess(from: response)
}

@MainActor
Expand Down
6 changes: 4 additions & 2 deletions apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
}

func compactSession(sessionKey: String) async throws {
_ = try await GatewayConnection.shared.request(
let response = try await GatewayConnection.shared.request(
method: "sessions.compact",
params: ["key": AnyCodable(sessionKey)],
timeoutMs: 10000)
timeoutMs: 0,
retryTransportFailures: false)
try OpenClawSessionsCompactResponse.requireSuccess(from: response)
}

func setActiveSessionKey(_ sessionKey: String) async throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ struct GatewayConnectionTests {
#expect(session.snapshotMakeCount() == 1)
}

@Test func `request can disable retries for non idempotent mutations`() async throws {
let session = GatewayTestWebSocketSession(
taskFactory: {
GatewayTestWebSocketTask(sendHook: { _, _, sendIndex in
if sendIndex > 0 {
throw URLError(.timedOut)
}
})
})
let (conn, _) = try self.makeConnection(session: session)

do {
_ = try await conn.request(
method: "sessions.compact",
params: nil,
timeoutMs: 10,
retryTransportFailures: false)
Issue.record("expected sessions.compact transport failure")
} catch {}

#expect(session.snapshotMakeCount() == 1)
#expect(session.latestTask()?.snapshotSendCount() == 2)
}

@Test func `subscribe replays latest snapshot`() async throws {
let session = self.makeSession()
let (conn, _) = try self.makeConnection(session: session)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ final class GatewayTestWebSocketTask: WebSocketTasking, @unchecked Sendable {
self.lock.withLock { self.connectRequestID }
}

func snapshotSendCount() -> Int {
self.lock.withLock { self.sendCount }
}

func resume() {
self.state = .running
}
Expand Down
17 changes: 12 additions & 5 deletions apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ private enum GatewayConnectErrorCodes {
}

public actor GatewayChannelActor {
nonisolated static func resolveRequestTimeoutMs(_ timeoutMs: Double?, defaultMs: Double) -> Double? {
timeoutMs == 0 ? nil : (timeoutMs ?? defaultMs)
}

private let logger = Logger(subsystem: "ai.openclaw", category: "gateway")
private var task: WebSocketTaskBox?
private var pending: [String: CheckedContinuation<GatewayFrame, Error>] = [:]
Expand Down Expand Up @@ -1174,14 +1178,17 @@ public actor GatewayChannelActor {
timeoutMs: Double? = nil) async throws -> Data
{
try await self.connectOrThrow(context: "gateway connect")
let effectiveTimeout = timeoutMs ?? self.defaultRequestTimeoutMs
// Zero leaves terminal-operation deadlines to the Gateway owner.
let effectiveTimeout = Self.resolveRequestTimeoutMs(timeoutMs, defaultMs: self.defaultRequestTimeoutMs)
let payload = try self.encodeRequest(method: method, params: params, kind: "request")
let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<GatewayFrame, Error>) in
self.pending[payload.id] = cont
Task { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000))
await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout)
if let effectiveTimeout {
Task { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000))
await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout)
}
}
Task {
do {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public struct OpenClawSessionsCompactResponse: Decodable, Sendable {
public let ok: Bool
public let reason: String?

public static func requireSuccess(from data: Data) throws {
let response = try JSONDecoder().decode(Self.self, from: data)
guard response.ok else {
throw OpenClawSessionsCompactError(reason: response.reason)
}
}
}

public struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable {
public let reason: String?

public var errorDescription: String? {
let detail = self.reason?.trimmingCharacters(in: .whitespacesAndNewlines)
return detail?.isEmpty == false ? detail : "Session compaction failed"
}

public init(reason: String?) {
self.reason = reason
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,7 @@ public struct SessionsListParams: Codable, Sendable {
public let spawnedby: String?
public let agentid: String?
public let search: String?
public let archived: Bool?

public init(
limit: Int?,
Expand All @@ -1653,7 +1654,8 @@ public struct SessionsListParams: Codable, Sendable {
label: String?,
spawnedby: String?,
agentid: String? = nil,
search: String?)
search: String?,
archived: Bool? = nil)
{
self.limit = limit
self.offset = offset
Expand All @@ -1667,6 +1669,7 @@ public struct SessionsListParams: Codable, Sendable {
self.spawnedby = spawnedby
self.agentid = agentid
self.search = search
self.archived = archived
}

private enum CodingKeys: String, CodingKey {
Expand All @@ -1682,6 +1685,7 @@ public struct SessionsListParams: Codable, Sendable {
case spawnedby = "spawnedBy"
case agentid = "agentId"
case search
case archived
}
}

Expand Down Expand Up @@ -2433,6 +2437,8 @@ public struct SessionsPatchParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let label: AnyCodable?
public let archived: Bool?
public let pinned: Bool?
public let thinkinglevel: AnyCodable?
public let fastmode: AnyCodable?
public let verboselevel: AnyCodable?
Expand Down Expand Up @@ -2460,6 +2466,8 @@ public struct SessionsPatchParams: Codable, Sendable {
key: String,
agentid: String? = nil,
label: AnyCodable?,
archived: Bool? = nil,
pinned: Bool? = nil,
thinkinglevel: AnyCodable?,
fastmode: AnyCodable?,
verboselevel: AnyCodable?,
Expand All @@ -2486,6 +2494,8 @@ public struct SessionsPatchParams: Codable, Sendable {
self.key = key
self.agentid = agentid
self.label = label
self.archived = archived
self.pinned = pinned
self.thinkinglevel = thinkinglevel
self.fastmode = fastmode
self.verboselevel = verboselevel
Expand Down Expand Up @@ -2514,6 +2524,8 @@ public struct SessionsPatchParams: Codable, Sendable {
case key
case agentid = "agentId"
case label
case archived
case pinned
case thinkinglevel = "thinkingLevel"
case fastmode = "fastMode"
case verboselevel = "verboseLevel"
Expand Down Expand Up @@ -2617,24 +2629,36 @@ public struct SessionsDeleteParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let deletetranscript: Bool?
public let expectedsessionid: String?
public let expectedlifecyclerevision: String?
public let expectedsessionupdatedat: Double?
public let emitlifecyclehooks: Bool?

public init(
key: String,
agentid: String? = nil,
deletetranscript: Bool?,
expectedsessionid: String? = nil,
expectedlifecyclerevision: String? = nil,
expectedsessionupdatedat: Double? = nil,
emitlifecyclehooks: Bool?)
{
self.key = key
self.agentid = agentid
self.deletetranscript = deletetranscript
self.expectedsessionid = expectedsessionid
self.expectedlifecyclerevision = expectedlifecyclerevision
self.expectedsessionupdatedat = expectedsessionupdatedat
self.emitlifecyclehooks = emitlifecyclehooks
}

private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case deletetranscript = "deleteTranscript"
case expectedsessionid = "expectedSessionId"
case expectedlifecyclerevision = "expectedLifecycleRevision"
case expectedsessionupdatedat = "expectedSessionUpdatedAt"
case emitlifecyclehooks = "emitLifecycleHooks"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,13 @@ struct GatewayNodeSessionTests {
#expect(response.error == nil)
}

@Test
func `gateway request timeout zero disables the client deadline`() {
#expect(GatewayChannelActor.resolveRequestTimeoutMs(0, defaultMs: 15000) == nil)
#expect(GatewayChannelActor.resolveRequestTimeoutMs(nil, defaultMs: 15000) == 15000)
#expect(GatewayChannelActor.resolveRequestTimeoutMs(30000, defaultMs: 15000) == 30000)
}

@Test
func `emits synthetic seq gap after reconnect snapshot`() async throws {
let session = FakeGatewayWebSocketSession()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import OpenClawKit
import Testing

struct SessionMutationResponsesTests {
@Test
func compactResponseAcceptsSuccess() throws {
try OpenClawSessionsCompactResponse.requireSuccess(
from: Data(#"{"ok":true,"key":"agent:main:main","compacted":true}"#.utf8))
}

@Test
func compactResponseSurfacesGatewayFailureReason() {
let data = Data(
#"{"ok":false,"key":"agent:main:main","compacted":false,"reason":"turn failed"}"#.utf8)
do {
try OpenClawSessionsCompactResponse.requireSuccess(
from: data)
Issue.record("expected failed compaction response to throw")
} catch let error as OpenClawSessionsCompactError {
#expect(error.errorDescription == "turn failed")
} catch {
Issue.record("unexpected error: \(error)")
}
}
}
4 changes: 2 additions & 2 deletions docs/.generated/config-baseline.sha256
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
acdf418738f79d29f58cb6cfe5b7ab2d353cbcce2252861a4f527f85ea0c54af config-baseline.json
f5e127927b1810ec9769f43ed15fbabde4c79a8778cc486e12119527df9b5ebb config-baseline.json
4c98c716bc78e65c274ec374757357c1dcc9b5ec75c9e00ea4c20851531b7d1a config-baseline.core.json
c68853362689981ac1cc1e55b9061286c2002104ff1c10bc44ee99a6080e169e config-baseline.channel.json
859aa272b0dad53b7080c6fefcf775347ae79a1998ec39dd18b732c90d9df90c config-baseline.plugin.json
4721a543fc05a2dee5d6af676a145069adf7849fe50e18c12a8515f9d9fe8b73 config-baseline.plugin.json
4 changes: 2 additions & 2 deletions docs/.generated/plugin-sdk-api-baseline.sha256
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
a4f18fb57ec58de32a554b6481190016e421246fef04fc8f8045271f4de843fe plugin-sdk-api-baseline.json
80d3e71a2183c95e0177df970429e639e9702a23a76bd7b852325eef32e7acd1 plugin-sdk-api-baseline.jsonl
95e17039bab1ad1dd3294948868740fb0315151b982e8f5c31859ba68fea2c0b plugin-sdk-api-baseline.json
1c936a4d3ffcf00a16df5aa9bbbba109ed856c4adfd6af6118841557e2e4ec88 plugin-sdk-api-baseline.jsonl
4 changes: 2 additions & 2 deletions docs/cli/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ openclaw sessions compact "agent:main:main" --max-lines 200
openclaw sessions compact "agent:work:main" --agent work --json
```

- Without `--max-lines`, the gateway LLM-summarizes the transcript. This can be slow, so the default `--timeout` is `180000` ms.
- Without `--max-lines`, the gateway LLM-summarizes the transcript. The CLI does not impose a client deadline by default; the gateway owns the configured compaction lifecycle.
- With `--max-lines <n>`, it truncates to the last `n` transcript lines and archives the prior transcript as a `.bak` sidecar.
- `--agent <id>`: agent that owns the session; required for `global` keys.
- `--url` / `--token` / `--password`: gateway connection overrides.
- `--timeout <ms>`: RPC timeout in milliseconds.
- `--timeout <ms>`: optional client-side RPC timeout in milliseconds.
- `--json`: print the raw RPC payload.

The command exits non-zero when the gateway reports a failed compaction or is unreachable, so crons and scripts never mistake a silent no-op for success.
Expand Down
Loading
Loading