agent-relay 3.2.14 → 3.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -54954,7 +54954,12 @@ var CLI_REGISTRY = {
54954
54954
  },
54955
54955
  codex: {
54956
54956
  binaries: ["codex"],
54957
- nonInteractiveArgs: (task, extra = []) => ["exec", task, ...extra],
54957
+ nonInteractiveArgs: (task, extra = []) => [
54958
+ "exec",
54959
+ "--dangerously-bypass-approvals-and-sandbox",
54960
+ task,
54961
+ ...extra
54962
+ ],
54958
54963
  bypassFlag: "--dangerously-bypass-approvals-and-sandbox",
54959
54964
  bypassAliases: ["--full-auto"],
54960
54965
  searchPaths: ["~/.local/bin"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Real-time agent-to-agent communication system",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -176,13 +176,13 @@
176
176
  },
177
177
  "homepage": "https://github.com/AgentWorkforce/relay#readme",
178
178
  "dependencies": {
179
- "@agent-relay/config": "3.2.14",
180
- "@agent-relay/hooks": "3.2.14",
181
- "@agent-relay/sdk": "3.2.14",
182
- "@agent-relay/telemetry": "3.2.14",
183
- "@agent-relay/trajectory": "3.2.14",
184
- "@agent-relay/user-directory": "3.2.14",
185
- "@agent-relay/utils": "3.2.14",
179
+ "@agent-relay/config": "3.2.15",
180
+ "@agent-relay/hooks": "3.2.15",
181
+ "@agent-relay/sdk": "3.2.15",
182
+ "@agent-relay/telemetry": "3.2.15",
183
+ "@agent-relay/trajectory": "3.2.15",
184
+ "@agent-relay/user-directory": "3.2.15",
185
+ "@agent-relay/utils": "3.2.15",
186
186
  "@modelcontextprotocol/sdk": "^1.0.0",
187
187
  "@relaycast/mcp": "1.0.0",
188
188
  "@relaycast/sdk": "1.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/acp-bridge",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "ACP (Agent Client Protocol) bridge for Agent Relay - expose relay agents to ACP-compatible editors like Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@agent-relay/sdk": "3.2.14",
49
+ "@agent-relay/sdk": "3.2.15",
50
50
  "@agentclientprotocol/sdk": "^0.12.0"
51
51
  },
52
52
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/config",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Shared configuration schemas and loaders for Agent Relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/hooks",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Hook emitter, registry, and trajectory hooks for Agent Relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,9 +37,9 @@
37
37
  "test:watch": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@agent-relay/config": "3.2.14",
41
- "@agent-relay/trajectory": "3.2.14",
42
- "@agent-relay/sdk": "3.2.14"
40
+ "@agent-relay/config": "3.2.15",
41
+ "@agent-relay/trajectory": "3.2.15",
42
+ "@agent-relay/sdk": "3.2.15"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/memory",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Semantic memory storage and retrieval system for agent-relay with multiple backend support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/hooks": "3.2.14"
25
+ "@agent-relay/hooks": "3.2.15"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/openclaw",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Relaycast bridge for OpenClaw — messaging, identity, runtime setup, and local spawning",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "postinstall": "node -e \"try{require('child_process').execSync('ldd --version 2>&1',{stdio:'pipe'})}catch{try{require('child_process').execSync('apk info gcompat 2>/dev/null',{stdio:'pipe'})}catch{console.warn('\\n\\u26a0\\ufe0f @agent-relay/openclaw: Alpine detected without gcompat. Spawning requires glibc.\\n Install with: apk add gcompat libstdc++\\n')}}\""
30
30
  },
31
31
  "dependencies": {
32
- "@agent-relay/sdk": "3.2.14",
32
+ "@agent-relay/sdk": "3.2.15",
33
33
  "@relaycast/sdk": "^1.0.0",
34
34
  "ws": "^8.0.0"
35
35
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/policy",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Agent policy management with multi-level fallback (repo, local PRPM, cloud workspace)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/config": "3.2.14"
25
+ "@agent-relay/config": "3.2.15"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1 +1 @@
1
- {"version":3,"file":"cli-registry.d.ts","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0DAA0D;IAC1D,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACrE,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sGAAsG;IACtG,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAQ/B,CAAC;AAwFF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAGvE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAE1E"}
1
+ {"version":3,"file":"cli-registry.d.ts","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0DAA0D;IAC1D,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACrE,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sGAAsG;IACtG,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAQ/B,CAAC;AA6FF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAGvE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAE1E"}
@@ -41,7 +41,12 @@ const CLI_REGISTRY = {
41
41
  },
42
42
  codex: {
43
43
  binaries: ['codex'],
44
- nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
44
+ nonInteractiveArgs: (task, extra = []) => [
45
+ 'exec',
46
+ '--dangerously-bypass-approvals-and-sandbox',
47
+ task,
48
+ ...extra,
49
+ ],
45
50
  bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
46
51
  bypassAliases: ['--full-auto'],
47
52
  searchPaths: ['~/.local/bin'],
@@ -1 +1 @@
1
- {"version":3,"file":"cli-registry.js","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,cAAc;IACd,iBAAiB;IACjB,iBAAiB;IACjB,gBAAgB;IAChB,UAAU;IACV,MAAM;IACN,mBAAmB;CACpB,CAAC;AAEF,8EAA8E;AAE9E,MAAM,YAAY,GAAoC;IACpD,MAAM,EAAE;QACN,QAAQ,EAAE,CAAC,QAAQ,CAAC;QACpB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,IAAI;YACJ,gCAAgC;YAChC,IAAI;YACJ,GAAG,KAAK;SACT;QACD,UAAU,EAAE,gCAAgC;QAC5C,WAAW,EAAE,CAAC,iBAAiB,CAAC;KACjC;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QAClE,UAAU,EAAE,4CAA4C;QACxD,aAAa,EAAE,CAAC,aAAa,CAAC;QAC9B,WAAW,EAAE,CAAC,cAAc,CAAC;KAC9B;IACD,MAAM,EAAE;QACN,QAAQ,EAAE,CAAC,QAAQ,CAAC;QACpB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QAChE,UAAU,EAAE,QAAQ;QACpB,aAAa,EAAE,CAAC,IAAI,CAAC;KACtB;IACD,QAAQ,EAAE;QACR,QAAQ,EAAE,CAAC,UAAU,CAAC;QACtB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QACjE,WAAW,EAAE,CAAC,iBAAiB,CAAC;QAChC,cAAc,EAAE,IAAI;KACrB;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;KACnE;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,WAAW;YACX,IAAI;YACJ,cAAc;YACd,UAAU;YACV,GAAG,KAAK;SACT;KACF;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,KAAK;YACL,QAAQ;YACR,IAAI;YACJ,cAAc;YACd,GAAG,KAAK;SACT;KACF;IACD,cAAc,EAAE;QACd,QAAQ,EAAE,CAAC,cAAc,CAAC;QAC1B,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,SAAS;YACT,IAAI;YACJ,IAAI;YACJ,GAAG,KAAK;SACT;KACF;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,SAAS;YACT,IAAI;YACJ,IAAI;YACJ,GAAG,KAAK;SACT;KACF;IACD,MAAM,EAAE;QACN,QAAQ,EAAE,CAAC,cAAc,EAAE,OAAO,CAAC;QACnC,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,SAAS;YACT,IAAI;YACJ,IAAI;YACJ,GAAG,KAAK;SACT;KACF;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5D,OAAO,YAAY,CAAC,OAAmB,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,YAAY,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"cli-registry.js","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -102,7 +102,7 @@
102
102
  "typescript": "^5.7.3"
103
103
  },
104
104
  "dependencies": {
105
- "@agent-relay/config": "3.2.14",
105
+ "@agent-relay/config": "3.2.15",
106
106
  "@relaycast/sdk": "^1.0.0",
107
107
  "@sinclair/typebox": "^0.34.48",
108
108
  "chalk": "^4.1.2",
@@ -983,10 +983,10 @@ agents:
983
983
  expect(args).toEqual(['-p', '--dangerously-skip-permissions', 'Do the thing']);
984
984
  });
985
985
 
986
- it('should build codex command with exec subcommand', () => {
986
+ it('should build codex command with exec subcommand and bypass flag', () => {
987
987
  const { cmd, args } = WorkflowRunner.buildNonInteractiveCommand('codex', 'Build it');
988
988
  expect(cmd).toBe('codex');
989
- expect(args).toEqual(['exec', 'Build it']);
989
+ expect(args).toEqual(['exec', '--dangerously-bypass-approvals-and-sandbox', 'Build it']);
990
990
  });
991
991
 
992
992
  it('should build gemini command with -p flag', () => {
@@ -64,7 +64,12 @@ const CLI_REGISTRY: Record<AgentCli, CliDefinition> = {
64
64
  },
65
65
  codex: {
66
66
  binaries: ['codex'],
67
- nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
67
+ nonInteractiveArgs: (task, extra = []) => [
68
+ 'exec',
69
+ '--dangerously-bypass-approvals-and-sandbox',
70
+ task,
71
+ ...extra,
72
+ ],
68
73
  bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
69
74
  bypassAliases: ['--full-auto'],
70
75
  searchPaths: ['~/.local/bin'],
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.2.14"
7
+ version = "3.2.15"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,323 @@
1
+ import Foundation
2
+
3
+ // MARK: - Delegate
4
+
5
+ @MainActor
6
+ public protocol RelayObserverDelegate: AnyObject {
7
+ func relayObserver(_ observer: RelayObserver, didReceiveEvent event: RelayObserverEvent)
8
+ func relayObserverDidConnect(_ observer: RelayObserver)
9
+ func relayObserverDidDisconnect(_ observer: RelayObserver, error: Error?)
10
+ }
11
+
12
+ // MARK: - RelayObserver
13
+
14
+ public final class RelayObserver: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
15
+
16
+ // MARK: - ConnectionState
17
+
18
+ public enum ConnectionState: Equatable, Sendable {
19
+ case disconnected
20
+ case connecting
21
+ case connected
22
+ case reconnecting(attempt: Int)
23
+ }
24
+
25
+ // MARK: - Public Properties (thread-safe via queue)
26
+
27
+ public var connectionState: ConnectionState {
28
+ queue.sync { _connectionState }
29
+ }
30
+ public var lastEvent: RelayObserverEvent? {
31
+ queue.sync { _lastEvent }
32
+ }
33
+ public var eventCounter: Int {
34
+ queue.sync { _eventCounter }
35
+ }
36
+ public weak var delegate: RelayObserverDelegate?
37
+
38
+ // MARK: - AsyncStream
39
+
40
+ public var events: AsyncStream<RelayObserverEvent> {
41
+ queue.sync {
42
+ _eventsContinuation?.finish()
43
+ return AsyncStream { continuation in
44
+ self._eventsContinuation = continuation
45
+ }
46
+ }
47
+ }
48
+
49
+ // MARK: - Private Properties
50
+
51
+ private let queue = DispatchQueue(label: "com.agentrelay.observer", qos: .userInitiated)
52
+ private var _connectionState: ConnectionState = .disconnected
53
+ private var _lastEvent: RelayObserverEvent?
54
+ private var _eventCounter: Int = 0
55
+ private var webSocketTask: URLSessionWebSocketTask?
56
+ private var urlSession: URLSession?
57
+ private let maxReconnectAttempts: Int
58
+ private let baseReconnectDelay: TimeInterval
59
+ private var reconnectAttempts: Int = 0
60
+ private var reconnectTask: Task<Void, Never>?
61
+ private var subscribedChannel: String?
62
+ private var pendingOutbound: [String] = []
63
+ private var isConnectionReady: Bool = false
64
+ private var activeURL: URL?
65
+ private var _eventsContinuation: AsyncStream<RelayObserverEvent>.Continuation?
66
+
67
+ private let jsonEncoder: JSONEncoder = {
68
+ let encoder = JSONEncoder()
69
+ encoder.keyEncodingStrategy = .convertToSnakeCase
70
+ return encoder
71
+ }()
72
+
73
+ private let jsonDecoder: JSONDecoder = {
74
+ // NOT using convertFromSnakeCase — we use explicit CodingKeys
75
+ // because of dual-name fields (name/agent_name, step/step_name)
76
+ let decoder = JSONDecoder()
77
+ return decoder
78
+ }()
79
+
80
+ // MARK: - Init
81
+
82
+ public init(maxReconnectAttempts: Int = 8, baseReconnectDelay: TimeInterval = 1.0) {
83
+ self.maxReconnectAttempts = maxReconnectAttempts
84
+ self.baseReconnectDelay = baseReconnectDelay
85
+ super.init()
86
+ }
87
+
88
+ // MARK: - Public Methods
89
+
90
+ /// Connect to a WebSocket proxy URL and subscribe to a channel on open.
91
+ public func connect(url: URL, channel: String) {
92
+ queue.sync {
93
+ self.subscribedChannel = channel
94
+ self._openSocket(url: url)
95
+ }
96
+ }
97
+
98
+ /// Connect to a WebSocket proxy URL without channel subscription.
99
+ public func connect(url: URL) {
100
+ queue.sync {
101
+ self.subscribedChannel = nil
102
+ self._openSocket(url: url)
103
+ }
104
+ }
105
+
106
+ /// Disconnect — closes socket, cancels reconnect, clears state.
107
+ public func disconnect() {
108
+ queue.sync {
109
+ reconnectTask?.cancel()
110
+ reconnectTask = nil
111
+ reconnectAttempts = 0
112
+ isConnectionReady = false
113
+ subscribedChannel = nil
114
+ pendingOutbound.removeAll()
115
+ activeURL = nil
116
+ _closeSocket(code: .goingAway, reason: nil)
117
+ _connectionState = .disconnected
118
+ _eventsContinuation?.finish()
119
+ _eventsContinuation = nil
120
+ }
121
+ }
122
+
123
+ /// Send a message to a channel through the proxy.
124
+ public func sendChannel(
125
+ channel: String,
126
+ text: String,
127
+ personas: [String]? = nil,
128
+ cliPreferences: [String: String]? = nil
129
+ ) throws {
130
+ let msg = ObserverChannelSendMessage(
131
+ channel: channel,
132
+ text: text,
133
+ personas: personas,
134
+ cliPreferences: cliPreferences
135
+ )
136
+ try queue.sync {
137
+ try _sendEncodable(msg)
138
+ }
139
+ }
140
+
141
+ /// Send a direct message to a specific agent through the proxy.
142
+ public func sendDirect(to: String, text: String) throws {
143
+ let msg = ObserverDirectSendMessage(to: to, text: text)
144
+ try queue.sync {
145
+ try _sendEncodable(msg)
146
+ }
147
+ }
148
+
149
+ // MARK: - Private Methods (must be called on queue)
150
+
151
+ private func _openSocket(url: URL) {
152
+ _closeSocket(code: .goingAway, reason: nil)
153
+ activeURL = url
154
+ _connectionState = .connecting
155
+ isConnectionReady = false
156
+
157
+ let delegateQueue = OperationQueue()
158
+ delegateQueue.underlyingQueue = queue
159
+ delegateQueue.maxConcurrentOperationCount = 1
160
+
161
+ let session = URLSession(
162
+ configuration: .default,
163
+ delegate: self,
164
+ delegateQueue: delegateQueue
165
+ )
166
+ self.urlSession = session
167
+ let task = session.webSocketTask(with: url)
168
+ self.webSocketTask = task
169
+ task.resume()
170
+ _scheduleReceive()
171
+ }
172
+
173
+ private func _closeSocket(code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
174
+ webSocketTask?.cancel(with: code, reason: reason)
175
+ webSocketTask = nil
176
+ urlSession?.invalidateAndCancel()
177
+ urlSession = nil
178
+ }
179
+
180
+ private func _scheduleReceive() {
181
+ webSocketTask?.receive { [weak self] result in
182
+ guard let self else { return }
183
+ // Callback arrives on our queue via delegateQueue
184
+ switch result {
185
+ case .success(let message):
186
+ self._handleMessage(message)
187
+ self._scheduleReceive()
188
+ case .failure(let error):
189
+ self._handleSocketError(error)
190
+ }
191
+ }
192
+ }
193
+
194
+ private func _handleMessage(_ message: URLSessionWebSocketTask.Message) {
195
+ let data: Data
196
+ switch message {
197
+ case .string(let text):
198
+ guard let d = text.data(using: .utf8) else { return }
199
+ data = d
200
+ case .data(let d):
201
+ data = d
202
+ @unknown default:
203
+ return
204
+ }
205
+
206
+ guard let event = try? jsonDecoder.decode(RelayObserverEvent.self, from: data) else { return }
207
+
208
+ _lastEvent = event
209
+ _eventCounter += 1
210
+
211
+ if let delegate {
212
+ Task { @MainActor in
213
+ delegate.relayObserver(self, didReceiveEvent: event)
214
+ }
215
+ }
216
+
217
+ _eventsContinuation?.yield(event)
218
+ }
219
+
220
+ private func _handleSocketError(_ error: Error) {
221
+ guard reconnectAttempts < maxReconnectAttempts else {
222
+ _connectionState = .disconnected
223
+ let delegate = self.delegate
224
+ Task { @MainActor in
225
+ delegate?.relayObserverDidDisconnect(self, error: error)
226
+ }
227
+ return
228
+ }
229
+
230
+ reconnectAttempts += 1
231
+ _connectionState = .reconnecting(attempt: reconnectAttempts)
232
+
233
+ let delay = baseReconnectDelay * pow(2.0, Double(reconnectAttempts - 1))
234
+
235
+ reconnectTask?.cancel()
236
+ reconnectTask = Task { [weak self] in
237
+ try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
238
+ guard !Task.isCancelled, let self else { return }
239
+ self.queue.sync {
240
+ guard let url = self.activeURL else { return }
241
+ self._openSocket(url: url)
242
+ }
243
+ }
244
+ }
245
+
246
+ private func _sendSubscription() {
247
+ guard let channel = subscribedChannel else { return }
248
+ let msg = ObserverSubscribeMessage(channel: channel)
249
+ if let data = try? jsonEncoder.encode(msg),
250
+ let str = String(data: data, encoding: .utf8) {
251
+ webSocketTask?.send(.string(str)) { _ in }
252
+ }
253
+ }
254
+
255
+ private func _sendEncodable<T: Encodable>(_ value: T) throws {
256
+ guard let task = webSocketTask else {
257
+ throw RelayObserverError.notConnected
258
+ }
259
+ guard let data = try? jsonEncoder.encode(value),
260
+ let str = String(data: data, encoding: .utf8) else {
261
+ throw RelayObserverError.encodingFailed
262
+ }
263
+
264
+ if isConnectionReady {
265
+ task.send(.string(str)) { _ in }
266
+ } else {
267
+ pendingOutbound.append(str)
268
+ }
269
+ }
270
+
271
+ private func _flushPendingOutbound() {
272
+ guard let task = webSocketTask else { return }
273
+ for str in pendingOutbound {
274
+ task.send(.string(str)) { _ in }
275
+ }
276
+ pendingOutbound.removeAll()
277
+ }
278
+
279
+ // MARK: - URLSessionWebSocketDelegate (callbacks arrive on queue via delegateQueue)
280
+
281
+ public func urlSession(
282
+ _ session: URLSession,
283
+ webSocketTask: URLSessionWebSocketTask,
284
+ didOpenWithProtocol protocol: String?
285
+ ) {
286
+ _connectionState = .connected
287
+ reconnectAttempts = 0
288
+ isConnectionReady = true
289
+
290
+ _flushPendingOutbound()
291
+ _sendSubscription()
292
+
293
+ let delegate = self.delegate
294
+ Task { @MainActor in
295
+ delegate?.relayObserverDidConnect(self)
296
+ }
297
+ }
298
+
299
+ public func urlSession(
300
+ _ session: URLSession,
301
+ webSocketTask: URLSessionWebSocketTask,
302
+ didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
303
+ reason: Data?
304
+ ) {
305
+ isConnectionReady = false
306
+
307
+ if closeCode != .goingAway && closeCode != .normalClosure {
308
+ _handleSocketError(
309
+ NSError(
310
+ domain: "RelayObserver",
311
+ code: Int(closeCode.rawValue),
312
+ userInfo: [NSLocalizedDescriptionKey: "WebSocket closed with code \(closeCode.rawValue)"]
313
+ )
314
+ )
315
+ } else {
316
+ _connectionState = .disconnected
317
+ let delegate = self.delegate
318
+ Task { @MainActor in
319
+ delegate?.relayObserverDidDisconnect(self, error: nil)
320
+ }
321
+ }
322
+ }
323
+ }
@@ -0,0 +1,143 @@
1
+ import Foundation
2
+
3
+ // MARK: - RelayObserverEventType
4
+
5
+ public enum RelayObserverEventType: String, Codable, Sendable {
6
+ case agentSpawned = "agent_spawned"
7
+ case agentReleased = "agent_released"
8
+ case agentIdle = "agent_idle"
9
+ case agentStatus = "agent_status"
10
+ case workerStream = "worker_stream"
11
+ case delivery = "delivery"
12
+ case channelMessage = "channel_message"
13
+ case stepStarted = "step_started"
14
+ case stepCompleted = "step_completed"
15
+ case runCompleted = "run_completed"
16
+ case relayConfig = "relay_config"
17
+ case relayWorkspace = "relay_workspace"
18
+ case commentPollTick = "comment_poll_tick"
19
+ case commentDetected = "comment_detected"
20
+ case error = "error"
21
+ case ack = "ack"
22
+ case connected = "connected"
23
+ case subscribed = "subscribed"
24
+ case pong = "pong"
25
+ }
26
+
27
+ // MARK: - RelayObserverEvent
28
+
29
+ public struct RelayObserverEvent: Decodable, Sendable {
30
+ public let type: RelayObserverEventType
31
+
32
+ // agent_spawned fields
33
+ public let name: String?
34
+ public let agentName: String?
35
+ public let cli: String?
36
+ public let channels: [String]?
37
+
38
+ // agent_released fields
39
+ public let reason: String?
40
+
41
+ // agent_idle fields
42
+ public let idleSecs: Int?
43
+
44
+ // agent_status fields
45
+ public let status: String?
46
+
47
+ // worker_stream fields
48
+ public let agent: String?
49
+ public let data: String?
50
+ public let stream: String?
51
+
52
+ // delivery fields
53
+ public let id: String?
54
+ public let from: String?
55
+ public let to: String?
56
+ public let text: String?
57
+ public let state: String?
58
+
59
+ // channel_message fields
60
+ public let channel: String?
61
+ public let timestamp: String?
62
+
63
+ // step_started / step_completed fields
64
+ public let step: String?
65
+ public let stepName: String?
66
+ public let output: String?
67
+
68
+ // run_completed fields
69
+ public let runId: String?
70
+
71
+ // relay_config fields
72
+ public let observerUrl: String?
73
+
74
+ // relay_workspace fields
75
+ public let workspaceId: String?
76
+
77
+ // comment_poll_tick fields
78
+ public let checkedAt: String?
79
+ public let intervalSeconds: Int?
80
+
81
+ // comment_detected / error fields
82
+ public let message: String?
83
+
84
+ enum CodingKeys: String, CodingKey {
85
+ case type
86
+ case name
87
+ case agentName = "agent_name"
88
+ case cli, channels, reason
89
+ case idleSecs = "idle_secs"
90
+ case status, agent, data, stream
91
+ case id, from, to, text, state
92
+ case channel, timestamp
93
+ case step
94
+ case stepName = "step_name"
95
+ case output
96
+ case runId = "run_id"
97
+ case observerUrl = "observer_url"
98
+ case workspaceId = "workspace_id"
99
+ case checkedAt = "checked_at"
100
+ case intervalSeconds = "interval_seconds"
101
+ case message
102
+ }
103
+ }
104
+
105
+ // MARK: - RelayObserverError
106
+
107
+ public enum RelayObserverError: LocalizedError, Sendable {
108
+ case notConnected
109
+ case encodingFailed
110
+
111
+ public var errorDescription: String? {
112
+ switch self {
113
+ case .notConnected: return "Relay not connected"
114
+ case .encodingFailed: return "Message encoding failed"
115
+ }
116
+ }
117
+ }
118
+
119
+ // MARK: - Outbound Message Structs (internal)
120
+
121
+ struct ObserverSubscribeMessage: Encodable {
122
+ let type: String = "subscribe"
123
+ let channel: String
124
+ }
125
+
126
+ struct ObserverChannelSendMessage: Encodable {
127
+ let type: String = "channel_send"
128
+ let channel: String
129
+ let text: String
130
+ let personas: [String]?
131
+ let cliPreferences: [String: String]?
132
+
133
+ enum CodingKeys: String, CodingKey {
134
+ case type, channel, text, personas
135
+ case cliPreferences = "cli_preferences"
136
+ }
137
+ }
138
+
139
+ struct ObserverDirectSendMessage: Encodable {
140
+ let type: String = "send"
141
+ let to: String
142
+ let text: String
143
+ }
@@ -0,0 +1,526 @@
1
+ import XCTest
2
+ @testable import AgentRelaySDK
3
+
4
+ // MARK: - Helpers
5
+
6
+ private func jsonData(_ jsonString: String) -> Data {
7
+ jsonString.data(using: .utf8)!
8
+ }
9
+
10
+ private func decodeEvent(_ json: String) throws -> RelayObserverEvent {
11
+ let decoder = JSONDecoder()
12
+ return try decoder.decode(RelayObserverEvent.self, from: jsonData(json))
13
+ }
14
+
15
+ private func encodeToDict<T: Encodable>(_ value: T) throws -> [String: Any] {
16
+ let encoder = JSONEncoder()
17
+ encoder.keyEncodingStrategy = .convertToSnakeCase
18
+ let data = try encoder.encode(value)
19
+ return try JSONSerialization.jsonObject(with: data) as! [String: Any]
20
+ }
21
+
22
+ // MARK: - Event Type Raw Values
23
+
24
+ final class RelayObserverEventTypeTests: XCTestCase {
25
+
26
+ func testAllRawValues() {
27
+ let expected: [(RelayObserverEventType, String)] = [
28
+ (.agentSpawned, "agent_spawned"),
29
+ (.agentReleased, "agent_released"),
30
+ (.agentIdle, "agent_idle"),
31
+ (.agentStatus, "agent_status"),
32
+ (.workerStream, "worker_stream"),
33
+ (.delivery, "delivery"),
34
+ (.channelMessage, "channel_message"),
35
+ (.stepStarted, "step_started"),
36
+ (.stepCompleted, "step_completed"),
37
+ (.runCompleted, "run_completed"),
38
+ (.relayConfig, "relay_config"),
39
+ (.relayWorkspace, "relay_workspace"),
40
+ (.commentPollTick, "comment_poll_tick"),
41
+ (.commentDetected, "comment_detected"),
42
+ (.error, "error"),
43
+ (.ack, "ack"),
44
+ (.connected, "connected"),
45
+ (.subscribed, "subscribed"),
46
+ (.pong, "pong"),
47
+ ]
48
+
49
+ XCTAssertEqual(expected.count, 19, "Should cover all 19 event types")
50
+
51
+ for (eventType, rawValue) in expected {
52
+ XCTAssertEqual(eventType.rawValue, rawValue, "Raw value mismatch for \(eventType)")
53
+ }
54
+ }
55
+
56
+ func testRoundTripCodable() throws {
57
+ let encoder = JSONEncoder()
58
+ let decoder = JSONDecoder()
59
+ for eventType in [RelayObserverEventType.agentSpawned, .delivery, .pong, .error] {
60
+ let data = try encoder.encode(eventType)
61
+ let decoded = try decoder.decode(RelayObserverEventType.self, from: data)
62
+ XCTAssertEqual(decoded, eventType)
63
+ }
64
+ }
65
+ }
66
+
67
+ // MARK: - Event Decoding Tests
68
+
69
+ final class RelayObserverEventDecodingTests: XCTestCase {
70
+
71
+ // MARK: agent_spawned
72
+
73
+ func testAgentSpawnedWithName() throws {
74
+ let event = try decodeEvent("""
75
+ {
76
+ "type": "agent_spawned",
77
+ "name": "worker-1",
78
+ "cli": "claude",
79
+ "channels": ["ch-1", "ch-2"]
80
+ }
81
+ """)
82
+ XCTAssertEqual(event.type, .agentSpawned)
83
+ XCTAssertEqual(event.name, "worker-1")
84
+ XCTAssertNil(event.agentName)
85
+ XCTAssertEqual(event.cli, "claude")
86
+ XCTAssertEqual(event.channels, ["ch-1", "ch-2"])
87
+ }
88
+
89
+ func testAgentSpawnedWithAgentName() throws {
90
+ let event = try decodeEvent("""
91
+ {
92
+ "type": "agent_spawned",
93
+ "agent_name": "worker-2",
94
+ "cli": "codex",
95
+ "channels": ["ch-3"]
96
+ }
97
+ """)
98
+ XCTAssertEqual(event.type, .agentSpawned)
99
+ XCTAssertNil(event.name)
100
+ XCTAssertEqual(event.agentName, "worker-2")
101
+ XCTAssertEqual(event.cli, "codex")
102
+ XCTAssertEqual(event.channels, ["ch-3"])
103
+ }
104
+
105
+ func testAgentSpawnedWithBothNames() throws {
106
+ let event = try decodeEvent("""
107
+ {
108
+ "type": "agent_spawned",
109
+ "name": "worker-a",
110
+ "agent_name": "worker-b",
111
+ "cli": "claude",
112
+ "channels": []
113
+ }
114
+ """)
115
+ XCTAssertEqual(event.name, "worker-a")
116
+ XCTAssertEqual(event.agentName, "worker-b")
117
+ }
118
+
119
+ // MARK: agent_released
120
+
121
+ func testAgentReleased() throws {
122
+ let event = try decodeEvent("""
123
+ {"type": "agent_released", "name": "worker-1", "reason": "task_complete"}
124
+ """)
125
+ XCTAssertEqual(event.type, .agentReleased)
126
+ XCTAssertEqual(event.name, "worker-1")
127
+ XCTAssertEqual(event.reason, "task_complete")
128
+ }
129
+
130
+ // MARK: agent_idle
131
+
132
+ func testAgentIdle() throws {
133
+ let event = try decodeEvent("""
134
+ {"type": "agent_idle", "name": "worker-1", "idle_secs": 120}
135
+ """)
136
+ XCTAssertEqual(event.type, .agentIdle)
137
+ XCTAssertEqual(event.name, "worker-1")
138
+ XCTAssertEqual(event.idleSecs, 120)
139
+ }
140
+
141
+ // MARK: agent_status
142
+
143
+ func testAgentStatus() throws {
144
+ let event = try decodeEvent("""
145
+ {"type": "agent_status", "name": "worker-1", "status": "busy"}
146
+ """)
147
+ XCTAssertEqual(event.type, .agentStatus)
148
+ XCTAssertEqual(event.name, "worker-1")
149
+ XCTAssertEqual(event.status, "busy")
150
+ }
151
+
152
+ // MARK: worker_stream
153
+
154
+ func testWorkerStream() throws {
155
+ let event = try decodeEvent("""
156
+ {"type": "worker_stream", "agent": "worker-1", "data": "hello world", "stream": "stdout"}
157
+ """)
158
+ XCTAssertEqual(event.type, .workerStream)
159
+ XCTAssertEqual(event.agent, "worker-1")
160
+ XCTAssertEqual(event.data, "hello world")
161
+ XCTAssertEqual(event.stream, "stdout")
162
+ }
163
+
164
+ // MARK: delivery
165
+
166
+ func testDeliveryCompleted() throws {
167
+ let event = try decodeEvent("""
168
+ {
169
+ "type": "delivery",
170
+ "id": "msg-1",
171
+ "from": "lead",
172
+ "to": "worker-1",
173
+ "text": "do this task",
174
+ "state": "completed"
175
+ }
176
+ """)
177
+ XCTAssertEqual(event.type, .delivery)
178
+ XCTAssertEqual(event.id, "msg-1")
179
+ XCTAssertEqual(event.from, "lead")
180
+ XCTAssertEqual(event.to, "worker-1")
181
+ XCTAssertEqual(event.text, "do this task")
182
+ XCTAssertEqual(event.state, "completed")
183
+ }
184
+
185
+ func testDeliveryFailed() throws {
186
+ let event = try decodeEvent("""
187
+ {
188
+ "type": "delivery",
189
+ "id": "msg-2",
190
+ "from": "lead",
191
+ "to": "worker-2",
192
+ "text": "another task",
193
+ "state": "failed"
194
+ }
195
+ """)
196
+ XCTAssertEqual(event.type, .delivery)
197
+ XCTAssertEqual(event.state, "failed")
198
+ }
199
+
200
+ // MARK: channel_message
201
+
202
+ func testChannelMessage() throws {
203
+ let event = try decodeEvent("""
204
+ {
205
+ "type": "channel_message",
206
+ "channel": "general",
207
+ "from": "user-1",
208
+ "text": "hey team",
209
+ "timestamp": "2026-03-23T10:00:00Z"
210
+ }
211
+ """)
212
+ XCTAssertEqual(event.type, .channelMessage)
213
+ XCTAssertEqual(event.channel, "general")
214
+ XCTAssertEqual(event.from, "user-1")
215
+ XCTAssertEqual(event.text, "hey team")
216
+ XCTAssertEqual(event.timestamp, "2026-03-23T10:00:00Z")
217
+ }
218
+
219
+ // MARK: step_started
220
+
221
+ func testStepStartedWithStep() throws {
222
+ let event = try decodeEvent("""
223
+ {"type": "step_started", "step": "build"}
224
+ """)
225
+ XCTAssertEqual(event.type, .stepStarted)
226
+ XCTAssertEqual(event.step, "build")
227
+ XCTAssertNil(event.stepName)
228
+ }
229
+
230
+ func testStepStartedWithStepName() throws {
231
+ let event = try decodeEvent("""
232
+ {"type": "step_started", "step_name": "compile"}
233
+ """)
234
+ XCTAssertEqual(event.type, .stepStarted)
235
+ XCTAssertNil(event.step)
236
+ XCTAssertEqual(event.stepName, "compile")
237
+ }
238
+
239
+ func testStepStartedWithBothStepFields() throws {
240
+ let event = try decodeEvent("""
241
+ {"type": "step_started", "step": "step-1", "step_name": "Build App"}
242
+ """)
243
+ XCTAssertEqual(event.step, "step-1")
244
+ XCTAssertEqual(event.stepName, "Build App")
245
+ }
246
+
247
+ // MARK: step_completed
248
+
249
+ func testStepCompleted() throws {
250
+ let event = try decodeEvent("""
251
+ {"type": "step_completed", "step": "build", "step_name": "Build App", "output": "success"}
252
+ """)
253
+ XCTAssertEqual(event.type, .stepCompleted)
254
+ XCTAssertEqual(event.step, "build")
255
+ XCTAssertEqual(event.stepName, "Build App")
256
+ XCTAssertEqual(event.output, "success")
257
+ }
258
+
259
+ // MARK: run_completed
260
+
261
+ func testRunCompleted() throws {
262
+ let event = try decodeEvent("""
263
+ {"type": "run_completed", "run_id": "run-abc-123"}
264
+ """)
265
+ XCTAssertEqual(event.type, .runCompleted)
266
+ XCTAssertEqual(event.runId, "run-abc-123")
267
+ }
268
+
269
+ // MARK: relay_config
270
+
271
+ func testRelayConfig() throws {
272
+ let event = try decodeEvent("""
273
+ {"type": "relay_config", "observer_url": "wss://relay.example.com/observe"}
274
+ """)
275
+ XCTAssertEqual(event.type, .relayConfig)
276
+ XCTAssertEqual(event.observerUrl, "wss://relay.example.com/observe")
277
+ }
278
+
279
+ // MARK: relay_workspace
280
+
281
+ func testRelayWorkspace() throws {
282
+ let event = try decodeEvent("""
283
+ {"type": "relay_workspace", "workspace_id": "ws-42"}
284
+ """)
285
+ XCTAssertEqual(event.type, .relayWorkspace)
286
+ XCTAssertEqual(event.workspaceId, "ws-42")
287
+ }
288
+
289
+ // MARK: comment_poll_tick
290
+
291
+ func testCommentPollTick() throws {
292
+ let event = try decodeEvent("""
293
+ {"type": "comment_poll_tick", "checked_at": "2026-03-23T12:00:00Z", "interval_seconds": 30}
294
+ """)
295
+ XCTAssertEqual(event.type, .commentPollTick)
296
+ XCTAssertEqual(event.checkedAt, "2026-03-23T12:00:00Z")
297
+ XCTAssertEqual(event.intervalSeconds, 30)
298
+ }
299
+
300
+ // MARK: comment_detected
301
+
302
+ func testCommentDetected() throws {
303
+ let event = try decodeEvent("""
304
+ {"type": "comment_detected", "message": "New comment on PR #42"}
305
+ """)
306
+ XCTAssertEqual(event.type, .commentDetected)
307
+ XCTAssertEqual(event.message, "New comment on PR #42")
308
+ }
309
+
310
+ // MARK: error
311
+
312
+ func testError() throws {
313
+ let event = try decodeEvent("""
314
+ {"type": "error", "message": "something went wrong"}
315
+ """)
316
+ XCTAssertEqual(event.type, .error)
317
+ XCTAssertEqual(event.message, "something went wrong")
318
+ }
319
+
320
+ // MARK: ack
321
+
322
+ func testAck() throws {
323
+ let event = try decodeEvent("""
324
+ {"type": "ack"}
325
+ """)
326
+ XCTAssertEqual(event.type, .ack)
327
+ }
328
+
329
+ // MARK: connected
330
+
331
+ func testConnected() throws {
332
+ let event = try decodeEvent("""
333
+ {"type": "connected"}
334
+ """)
335
+ XCTAssertEqual(event.type, .connected)
336
+ }
337
+
338
+ // MARK: subscribed
339
+
340
+ func testSubscribed() throws {
341
+ let event = try decodeEvent("""
342
+ {"type": "subscribed", "channel": "ops"}
343
+ """)
344
+ XCTAssertEqual(event.type, .subscribed)
345
+ XCTAssertEqual(event.channel, "ops")
346
+ }
347
+
348
+ // MARK: pong
349
+
350
+ func testPong() throws {
351
+ let event = try decodeEvent("""
352
+ {"type": "pong"}
353
+ """)
354
+ XCTAssertEqual(event.type, .pong)
355
+ }
356
+ }
357
+
358
+ // MARK: - Optional Fields (minimal event)
359
+
360
+ final class RelayObserverEventOptionalFieldsTests: XCTestCase {
361
+
362
+ func testMinimalAckHasAllOptionalsNil() throws {
363
+ let event = try decodeEvent("""
364
+ {"type": "ack"}
365
+ """)
366
+ XCTAssertEqual(event.type, .ack)
367
+ XCTAssertNil(event.name)
368
+ XCTAssertNil(event.agentName)
369
+ XCTAssertNil(event.cli)
370
+ XCTAssertNil(event.channels)
371
+ XCTAssertNil(event.reason)
372
+ XCTAssertNil(event.idleSecs)
373
+ XCTAssertNil(event.status)
374
+ XCTAssertNil(event.agent)
375
+ XCTAssertNil(event.data)
376
+ XCTAssertNil(event.stream)
377
+ XCTAssertNil(event.id)
378
+ XCTAssertNil(event.from)
379
+ XCTAssertNil(event.to)
380
+ XCTAssertNil(event.text)
381
+ XCTAssertNil(event.state)
382
+ XCTAssertNil(event.channel)
383
+ XCTAssertNil(event.timestamp)
384
+ XCTAssertNil(event.step)
385
+ XCTAssertNil(event.stepName)
386
+ XCTAssertNil(event.output)
387
+ XCTAssertNil(event.runId)
388
+ XCTAssertNil(event.observerUrl)
389
+ XCTAssertNil(event.workspaceId)
390
+ XCTAssertNil(event.checkedAt)
391
+ XCTAssertNil(event.intervalSeconds)
392
+ XCTAssertNil(event.message)
393
+ }
394
+ }
395
+
396
+ // MARK: - Outbound Message Encoding
397
+
398
+ final class RelayObserverOutboundEncodingTests: XCTestCase {
399
+
400
+ func testSubscribeMessage() throws {
401
+ let msg = ObserverSubscribeMessage(channel: "ops")
402
+ let dict = try encodeToDict(msg)
403
+ XCTAssertEqual(dict["type"] as? String, "subscribe")
404
+ XCTAssertEqual(dict["channel"] as? String, "ops")
405
+ XCTAssertEqual(dict.count, 2)
406
+ }
407
+
408
+ func testChannelSendMessageFull() throws {
409
+ let msg = ObserverChannelSendMessage(
410
+ channel: "general",
411
+ text: "hello",
412
+ personas: ["lead", "worker"],
413
+ cliPreferences: ["model": "opus"]
414
+ )
415
+ let dict = try encodeToDict(msg)
416
+ XCTAssertEqual(dict["type"] as? String, "channel_send")
417
+ XCTAssertEqual(dict["channel"] as? String, "general")
418
+ XCTAssertEqual(dict["text"] as? String, "hello")
419
+ XCTAssertEqual(dict["personas"] as? [String], ["lead", "worker"])
420
+ let prefs = dict["cli_preferences"] as? [String: String]
421
+ XCTAssertEqual(prefs?["model"], "opus")
422
+ }
423
+
424
+ func testChannelSendMessageMinimal() throws {
425
+ let msg = ObserverChannelSendMessage(
426
+ channel: "ops",
427
+ text: "ping",
428
+ personas: nil,
429
+ cliPreferences: nil
430
+ )
431
+ let dict = try encodeToDict(msg)
432
+ XCTAssertEqual(dict["type"] as? String, "channel_send")
433
+ XCTAssertEqual(dict["channel"] as? String, "ops")
434
+ XCTAssertEqual(dict["text"] as? String, "ping")
435
+ // nil optionals should not be present
436
+ XCTAssertNil(dict["personas"])
437
+ XCTAssertNil(dict["cli_preferences"])
438
+ }
439
+
440
+ func testDirectSendMessage() throws {
441
+ let msg = ObserverDirectSendMessage(to: "worker-1", text: "do it")
442
+ let dict = try encodeToDict(msg)
443
+ XCTAssertEqual(dict["type"] as? String, "send")
444
+ XCTAssertEqual(dict["to"] as? String, "worker-1")
445
+ XCTAssertEqual(dict["text"] as? String, "do it")
446
+ XCTAssertEqual(dict.count, 3)
447
+ }
448
+ }
449
+
450
+ // MARK: - RelayObserverError Tests
451
+
452
+ final class RelayObserverErrorTests: XCTestCase {
453
+
454
+ func testNotConnectedDescription() {
455
+ let error = RelayObserverError.notConnected
456
+ XCTAssertEqual(error.errorDescription, "Relay not connected")
457
+ XCTAssertEqual(error.localizedDescription, "Relay not connected")
458
+ }
459
+
460
+ func testEncodingFailedDescription() {
461
+ let error = RelayObserverError.encodingFailed
462
+ XCTAssertEqual(error.errorDescription, "Message encoding failed")
463
+ XCTAssertEqual(error.localizedDescription, "Message encoding failed")
464
+ }
465
+ }
466
+
467
+ // MARK: - ConnectionState Tests
468
+
469
+ final class RelayObserverConnectionStateTests: XCTestCase {
470
+
471
+ func testEquatable() {
472
+ XCTAssertEqual(RelayObserver.ConnectionState.disconnected, .disconnected)
473
+ XCTAssertEqual(RelayObserver.ConnectionState.connecting, .connecting)
474
+ XCTAssertEqual(RelayObserver.ConnectionState.connected, .connected)
475
+ XCTAssertEqual(
476
+ RelayObserver.ConnectionState.reconnecting(attempt: 3),
477
+ .reconnecting(attempt: 3)
478
+ )
479
+ }
480
+
481
+ func testNotEqual() {
482
+ XCTAssertNotEqual(RelayObserver.ConnectionState.disconnected, .connecting)
483
+ XCTAssertNotEqual(RelayObserver.ConnectionState.connected, .disconnected)
484
+ XCTAssertNotEqual(
485
+ RelayObserver.ConnectionState.reconnecting(attempt: 1),
486
+ .reconnecting(attempt: 2)
487
+ )
488
+ XCTAssertNotEqual(
489
+ RelayObserver.ConnectionState.reconnecting(attempt: 1),
490
+ .connected
491
+ )
492
+ }
493
+ }
494
+
495
+ // MARK: - RelayObserver Init Tests
496
+
497
+ final class RelayObserverInitTests: XCTestCase {
498
+
499
+ func testDefaultState() {
500
+ let observer = RelayObserver()
501
+ XCTAssertEqual(observer.connectionState, .disconnected)
502
+ XCTAssertEqual(observer.eventCounter, 0)
503
+ XCTAssertNil(observer.lastEvent)
504
+ }
505
+
506
+ func testCustomInit() {
507
+ let observer = RelayObserver(maxReconnectAttempts: 3, baseReconnectDelay: 2.0)
508
+ XCTAssertEqual(observer.connectionState, .disconnected)
509
+ XCTAssertEqual(observer.eventCounter, 0)
510
+ XCTAssertNil(observer.lastEvent)
511
+ }
512
+
513
+ func testSendChannelThrowsWhenDisconnected() {
514
+ let observer = RelayObserver()
515
+ XCTAssertThrowsError(try observer.sendChannel(channel: "ops", text: "hi")) { error in
516
+ XCTAssertEqual(error as? RelayObserverError, .notConnected)
517
+ }
518
+ }
519
+
520
+ func testSendDirectThrowsWhenDisconnected() {
521
+ let observer = RelayObserver()
522
+ XCTAssertThrowsError(try observer.sendDirect(to: "agent", text: "hi")) { error in
523
+ XCTAssertEqual(error as? RelayObserverError, .notConnected)
524
+ }
525
+ }
526
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/trajectory",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Trajectory integration utilities (trail/PDERO) for Relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/config": "3.2.14"
25
+ "@agent-relay/config": "3.2.15"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/user-directory",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "User directory service for agent-relay (per-user credential storage)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/utils": "3.2.14"
25
+ "@agent-relay/utils": "3.2.15"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/utils",
3
- "version": "3.2.14",
3
+ "version": "3.2.15",
4
4
  "description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -112,7 +112,7 @@
112
112
  "vitest": "^3.2.4"
113
113
  },
114
114
  "dependencies": {
115
- "@agent-relay/config": "3.2.14",
115
+ "@agent-relay/config": "3.2.15",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {