agent-relay 3.2.22 → 4.0.1

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.
Files changed (232) hide show
  1. package/README.md +5 -5
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/index.cjs +6564 -2100
  7. package/dist/src/cli/bootstrap.d.ts.map +1 -1
  8. package/dist/src/cli/bootstrap.js +2 -0
  9. package/dist/src/cli/bootstrap.js.map +1 -1
  10. package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
  11. package/dist/src/cli/commands/agent-management.js +14 -4
  12. package/dist/src/cli/commands/agent-management.js.map +1 -1
  13. package/dist/src/cli/commands/core.d.ts +2 -6
  14. package/dist/src/cli/commands/core.d.ts.map +1 -1
  15. package/dist/src/cli/commands/core.js +31 -12
  16. package/dist/src/cli/commands/core.js.map +1 -1
  17. package/dist/src/cli/commands/messaging.d.ts.map +1 -1
  18. package/dist/src/cli/commands/messaging.js +10 -3
  19. package/dist/src/cli/commands/messaging.js.map +1 -1
  20. package/dist/src/cli/commands/monitoring.d.ts +2 -2
  21. package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
  22. package/dist/src/cli/commands/monitoring.js +15 -6
  23. package/dist/src/cli/commands/monitoring.js.map +1 -1
  24. package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
  25. package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
  26. package/dist/src/cli/commands/on/dotfiles.js +157 -0
  27. package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
  28. package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
  29. package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
  30. package/dist/src/cli/commands/on/prereqs.js +103 -0
  31. package/dist/src/cli/commands/on/prereqs.js.map +1 -0
  32. package/dist/src/cli/commands/on/provision.d.ts +22 -0
  33. package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
  34. package/dist/src/cli/commands/on/provision.js +157 -0
  35. package/dist/src/cli/commands/on/provision.js.map +1 -0
  36. package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
  37. package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
  38. package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
  39. package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
  40. package/dist/src/cli/commands/on/scan.d.ts +8 -0
  41. package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
  42. package/dist/src/cli/commands/on/scan.js +59 -0
  43. package/dist/src/cli/commands/on/scan.js.map +1 -0
  44. package/dist/src/cli/commands/on/services.d.ts +17 -0
  45. package/dist/src/cli/commands/on/services.d.ts.map +1 -0
  46. package/dist/src/cli/commands/on/services.js +328 -0
  47. package/dist/src/cli/commands/on/services.js.map +1 -0
  48. package/dist/src/cli/commands/on/start.d.ts +61 -0
  49. package/dist/src/cli/commands/on/start.d.ts.map +1 -0
  50. package/dist/src/cli/commands/on/start.js +1107 -0
  51. package/dist/src/cli/commands/on/start.js.map +1 -0
  52. package/dist/src/cli/commands/on/stop.d.ts +4 -0
  53. package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
  54. package/dist/src/cli/commands/on/stop.js +11 -0
  55. package/dist/src/cli/commands/on/stop.js.map +1 -0
  56. package/dist/src/cli/commands/on/token.d.ts +8 -0
  57. package/dist/src/cli/commands/on/token.d.ts.map +1 -0
  58. package/dist/src/cli/commands/on/token.js +26 -0
  59. package/dist/src/cli/commands/on/token.js.map +1 -0
  60. package/dist/src/cli/commands/on/workspace.d.ts +4 -0
  61. package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
  62. package/dist/src/cli/commands/on/workspace.js +245 -0
  63. package/dist/src/cli/commands/on/workspace.js.map +1 -0
  64. package/dist/src/cli/commands/on.d.ts +10 -0
  65. package/dist/src/cli/commands/on.d.ts.map +1 -0
  66. package/dist/src/cli/commands/on.js +52 -0
  67. package/dist/src/cli/commands/on.js.map +1 -0
  68. package/dist/src/cli/commands/setup.d.ts.map +1 -1
  69. package/dist/src/cli/commands/setup.js +10 -21
  70. package/dist/src/cli/commands/setup.js.map +1 -1
  71. package/dist/src/cli/lib/bridge.js +1 -1
  72. package/dist/src/cli/lib/bridge.js.map +1 -1
  73. package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
  74. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  75. package/dist/src/cli/lib/broker-lifecycle.js +82 -120
  76. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  77. package/dist/src/cli/lib/client-factory.d.ts +4 -4
  78. package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
  79. package/dist/src/cli/lib/client-factory.js +14 -11
  80. package/dist/src/cli/lib/client-factory.js.map +1 -1
  81. package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
  82. package/dist/src/cli/lib/core-maintenance.js +11 -22
  83. package/dist/src/cli/lib/core-maintenance.js.map +1 -1
  84. package/dist/src/cost/pricing.d.ts +18 -0
  85. package/dist/src/cost/pricing.d.ts.map +1 -0
  86. package/dist/src/cost/pricing.js +111 -0
  87. package/dist/src/cost/pricing.js.map +1 -0
  88. package/dist/src/cost/tracker.d.ts +13 -0
  89. package/dist/src/cost/tracker.d.ts.map +1 -0
  90. package/dist/src/cost/tracker.js +152 -0
  91. package/dist/src/cost/tracker.js.map +1 -0
  92. package/dist/src/cost/types.d.ts +23 -0
  93. package/dist/src/cost/types.d.ts.map +1 -0
  94. package/dist/src/cost/types.js +2 -0
  95. package/dist/src/cost/types.js.map +1 -0
  96. package/package.json +15 -12
  97. package/packages/acp-bridge/package.json +2 -2
  98. package/packages/brand/package.json +1 -1
  99. package/packages/cloud/package.json +3 -3
  100. package/packages/config/package.json +1 -1
  101. package/packages/hooks/package.json +4 -4
  102. package/packages/memory/package.json +2 -2
  103. package/packages/openclaw/package.json +2 -2
  104. package/packages/policy/package.json +2 -2
  105. package/packages/sdk/README.md +10 -3
  106. package/packages/sdk/dist/broker-path.d.ts +3 -2
  107. package/packages/sdk/dist/broker-path.d.ts.map +1 -1
  108. package/packages/sdk/dist/broker-path.js +119 -32
  109. package/packages/sdk/dist/broker-path.js.map +1 -1
  110. package/packages/sdk/dist/client.d.ts +119 -197
  111. package/packages/sdk/dist/client.d.ts.map +1 -1
  112. package/packages/sdk/dist/client.js +354 -823
  113. package/packages/sdk/dist/client.js.map +1 -1
  114. package/packages/sdk/dist/examples/example.js +2 -5
  115. package/packages/sdk/dist/examples/example.js.map +1 -1
  116. package/packages/sdk/dist/index.d.ts +3 -1
  117. package/packages/sdk/dist/index.d.ts.map +1 -1
  118. package/packages/sdk/dist/index.js +3 -1
  119. package/packages/sdk/dist/index.js.map +1 -1
  120. package/packages/sdk/dist/relay-adapter.d.ts +9 -26
  121. package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
  122. package/packages/sdk/dist/relay-adapter.js +75 -47
  123. package/packages/sdk/dist/relay-adapter.js.map +1 -1
  124. package/packages/sdk/dist/relay.d.ts +26 -6
  125. package/packages/sdk/dist/relay.d.ts.map +1 -1
  126. package/packages/sdk/dist/relay.js +213 -43
  127. package/packages/sdk/dist/relay.js.map +1 -1
  128. package/packages/sdk/dist/transport.d.ts +58 -0
  129. package/packages/sdk/dist/transport.d.ts.map +1 -0
  130. package/packages/sdk/dist/transport.js +184 -0
  131. package/packages/sdk/dist/transport.js.map +1 -0
  132. package/packages/sdk/dist/types.d.ts +69 -0
  133. package/packages/sdk/dist/types.d.ts.map +1 -0
  134. package/packages/sdk/dist/types.js +5 -0
  135. package/packages/sdk/dist/types.js.map +1 -0
  136. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
  137. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
  138. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
  139. package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
  140. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
  141. package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
  142. package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
  143. package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
  144. package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
  145. package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
  146. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
  147. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
  148. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
  149. package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
  150. package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
  151. package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
  152. package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
  153. package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
  154. package/packages/sdk/dist/workflows/builder.d.ts +3 -2
  155. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  156. package/packages/sdk/dist/workflows/builder.js +1 -3
  157. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  158. package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
  159. package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
  160. package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
  161. package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
  162. package/packages/sdk/dist/workflows/index.d.ts +7 -0
  163. package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
  164. package/packages/sdk/dist/workflows/index.js +7 -0
  165. package/packages/sdk/dist/workflows/index.js.map +1 -1
  166. package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
  167. package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
  168. package/packages/sdk/dist/workflows/process-spawner.js +141 -0
  169. package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
  170. package/packages/sdk/dist/workflows/run.d.ts +2 -1
  171. package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
  172. package/packages/sdk/dist/workflows/run.js.map +1 -1
  173. package/packages/sdk/dist/workflows/runner.d.ts +6 -6
  174. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  175. package/packages/sdk/dist/workflows/runner.js +443 -719
  176. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  177. package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
  178. package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
  179. package/packages/sdk/dist/workflows/step-executor.js +393 -0
  180. package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
  181. package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
  182. package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
  183. package/packages/sdk/dist/workflows/template-resolver.js +144 -0
  184. package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
  185. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  186. package/packages/sdk/dist/workflows/validator.js +17 -2
  187. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  188. package/packages/sdk/dist/workflows/verification.d.ts +33 -0
  189. package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
  190. package/packages/sdk/dist/workflows/verification.js +122 -0
  191. package/packages/sdk/dist/workflows/verification.js.map +1 -0
  192. package/packages/sdk/package.json +2 -2
  193. package/packages/sdk/src/__tests__/unit.test.ts +100 -1
  194. package/packages/sdk/src/broker-path.ts +136 -30
  195. package/packages/sdk/src/client.ts +453 -1069
  196. package/packages/sdk/src/examples/example.ts +2 -5
  197. package/packages/sdk/src/index.ts +9 -1
  198. package/packages/sdk/src/relay-adapter.ts +75 -55
  199. package/packages/sdk/src/relay.ts +262 -55
  200. package/packages/sdk/src/transport.ts +216 -0
  201. package/packages/sdk/src/types.ts +75 -0
  202. package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
  203. package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
  204. package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
  205. package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
  206. package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
  207. package/packages/sdk/src/workflows/builder.ts +6 -6
  208. package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
  209. package/packages/sdk/src/workflows/index.ts +12 -0
  210. package/packages/sdk/src/workflows/process-spawner.ts +201 -0
  211. package/packages/sdk/src/workflows/run.ts +2 -1
  212. package/packages/sdk/src/workflows/runner.ts +636 -951
  213. package/packages/sdk/src/workflows/step-executor.ts +579 -0
  214. package/packages/sdk/src/workflows/template-resolver.ts +180 -0
  215. package/packages/sdk/src/workflows/validator.ts +20 -2
  216. package/packages/sdk/src/workflows/verification.ts +184 -0
  217. package/packages/sdk-py/pyproject.toml +1 -1
  218. package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
  219. package/packages/sdk-py/src/agent_relay/client.py +329 -522
  220. package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
  221. package/packages/sdk-py/src/agent_relay/relay.py +1 -4
  222. package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
  223. package/packages/sdk-py/uv.lock +5388 -0
  224. package/packages/telemetry/dist/client.d.ts.map +1 -1
  225. package/packages/telemetry/dist/client.js +1 -1
  226. package/packages/telemetry/dist/client.js.map +1 -1
  227. package/packages/telemetry/package.json +1 -1
  228. package/packages/telemetry/src/client.ts +3 -10
  229. package/packages/trajectory/package.json +2 -2
  230. package/packages/user-directory/package.json +2 -2
  231. package/packages/utils/package.json +2 -2
  232. package/scripts/postinstall.js +121 -1
@@ -24,15 +24,18 @@
24
24
  */
25
25
 
26
26
  import { randomBytes } from 'node:crypto';
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
27
28
  import path from 'node:path';
28
29
 
30
+ import { RelayCast } from '@relaycast/sdk';
31
+
29
32
  import {
30
33
  AgentRelayClient,
31
- AgentRelayProtocolError,
32
- type AgentRelayClientOptions,
33
- type SendMessageInput,
34
- type SpawnPtyInput,
34
+ type AgentRelayBrokerInitArgs,
35
+ type AgentRelaySpawnOptions,
35
36
  } from './client.js';
37
+ import { AgentRelayProtocolError } from './transport.js';
38
+ import type { SendMessageInput, SpawnPtyInput } from './types.js';
36
39
  import type {
37
40
  AgentRuntime,
38
41
  BrokerEvent,
@@ -75,6 +78,71 @@ function buildUnsupportedOperationMessage(
75
78
  };
76
79
  }
77
80
 
81
+ interface WorkspaceRegistryEntry {
82
+ relaycastApiKey?: string;
83
+ relayfileUrl?: string;
84
+ createdAt?: string;
85
+ agents?: string[];
86
+ }
87
+
88
+ type WorkspaceRegistry = Record<string, WorkspaceRegistryEntry>;
89
+
90
+ const WORKSPACE_ID_PREFIX = 'rw_';
91
+ const WORKSPACE_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
92
+
93
+ function normalizeWorkspaceId(value: string | undefined): string | undefined {
94
+ const trimmed = value?.trim();
95
+ return trimmed ? trimmed : undefined;
96
+ }
97
+
98
+ function generateWorkspaceId(): string {
99
+ const alphabetLength = WORKSPACE_ID_ALPHABET.length;
100
+ const maxUnbiasedValue = Math.floor(256 / alphabetLength) * alphabetLength;
101
+ let suffix = '';
102
+
103
+ while (suffix.length < 8) {
104
+ const bytes = randomBytes(8 - suffix.length);
105
+ for (const byte of bytes) {
106
+ if (byte >= maxUnbiasedValue) continue;
107
+ suffix += WORKSPACE_ID_ALPHABET[byte % alphabetLength];
108
+ if (suffix.length === 8) break;
109
+ }
110
+ }
111
+
112
+ return `${WORKSPACE_ID_PREFIX}${suffix}`;
113
+ }
114
+
115
+ function toWorkspaceRegistryEntry(value: unknown): WorkspaceRegistryEntry {
116
+ if (!value || typeof value !== 'object') {
117
+ return {};
118
+ }
119
+
120
+ const record = value as Record<string, unknown>;
121
+ const relaycastApiKey =
122
+ typeof record.relaycastApiKey === 'string' && record.relaycastApiKey.trim()
123
+ ? record.relaycastApiKey.trim()
124
+ : undefined;
125
+ const relayfileUrl =
126
+ typeof record.relayfileUrl === 'string' && record.relayfileUrl.trim()
127
+ ? record.relayfileUrl.trim()
128
+ : undefined;
129
+ const createdAt =
130
+ typeof record.createdAt === 'string' && record.createdAt.trim() ? record.createdAt.trim() : undefined;
131
+ const agents = Array.isArray(record.agents)
132
+ ? record.agents
133
+ .filter((agent): agent is string => typeof agent === 'string')
134
+ .map((agent) => agent.trim())
135
+ .filter((agent) => agent.length > 0)
136
+ : undefined;
137
+
138
+ return {
139
+ ...(relaycastApiKey ? { relaycastApiKey } : {}),
140
+ ...(relayfileUrl ? { relayfileUrl } : {}),
141
+ ...(createdAt ? { createdAt } : {}),
142
+ ...(agents && agents.length > 0 ? { agents } : {}),
143
+ };
144
+ }
145
+
78
146
  // ── Public types ────────────────────────────────────────────────────────────
79
147
 
80
148
  export interface Message {
@@ -201,7 +269,10 @@ export interface Agent {
201
269
  * @param options.stream — if provided, only invoke callback when the event stream matches (e.g. 'stdout', 'stderr')
202
270
  * @param options.mode — 'chunk' for raw string callbacks, 'structured' for { stream, chunk } callbacks. Auto-detected if omitted.
203
271
  */
204
- onOutput(callback: AgentOutputCallback, options?: { stream?: string; mode?: 'chunk' | 'structured' }): () => void;
272
+ onOutput(
273
+ callback: AgentOutputCallback,
274
+ options?: { stream?: string; mode?: 'chunk' | 'structured' }
275
+ ): () => void;
205
276
  }
206
277
 
207
278
  export interface HumanHandle {
@@ -236,17 +307,24 @@ export type EventHook<T> = ((value: T) => void) | null;
236
307
 
237
308
  export interface AgentRelayOptions {
238
309
  binaryPath?: string;
239
- binaryArgs?: string[];
310
+ binaryArgs?: AgentRelayBrokerInitArgs;
240
311
  brokerName?: string;
241
312
  channels?: string[];
242
313
  cwd?: string;
243
314
  env?: NodeJS.ProcessEnv;
244
315
  requestTimeoutMs?: number;
245
- shutdownTimeoutMs?: number;
246
316
  /**
247
- * Name for the auto-created Relaycast workspace.
248
- * If omitted, a random name is generated.
249
- * Ignored when RELAY_API_KEY is already set in env or process.env.
317
+ * Unified workspace ID shared across relayfile, relayauth claims, and
318
+ * relaycast key lookup.
319
+ */
320
+ workspaceId?: string;
321
+ /**
322
+ * Display name for an auto-created Relaycast workspace.
323
+ * If omitted, the unified workspace ID is used.
324
+ *
325
+ * @deprecated Since v1.x this field falls back to workspaceId when omitted,
326
+ * changing prior behavior where it was required for workspace naming.
327
+ * Callers relying on distinct naming should set this explicitly.
250
328
  */
251
329
  workspaceName?: string;
252
330
  /**
@@ -302,14 +380,17 @@ export class AgentRelay {
302
380
  readonly gemini: AgentSpawner;
303
381
  readonly opencode: AgentSpawner;
304
382
 
305
- private readonly clientOptions: AgentRelayClientOptions;
383
+ private readonly clientOptions: AgentRelaySpawnOptions;
306
384
  private readonly defaultChannels: string[];
385
+ private readonly requestedWorkspaceId?: string;
307
386
  private readonly workspaceName?: string;
308
387
  private readonly relaycastBaseUrl?: string;
309
388
  private relayApiKey?: string;
389
+ private resolvedWorkspaceId?: string;
310
390
  private client?: AgentRelayClient;
311
391
  private startPromise?: Promise<AgentRelayClient>;
312
392
  private unsubEvent?: () => void;
393
+ private readonly stderrListeners = new Set<(line: string) => void>();
313
394
  private readonly knownAgents = new Map<string, Agent>();
314
395
  private readonly readyAgents = new Set<string>();
315
396
  private readonly messageReadyAgents = new Set<string>();
@@ -329,18 +410,25 @@ export class AgentRelay {
329
410
  private idleResolverSeq = 0;
330
411
 
331
412
  constructor(options: AgentRelayOptions = {}) {
413
+ const requestedWorkspaceId = normalizeWorkspaceId(options.workspaceId);
332
414
  this.defaultChannels = options.channels ?? ['general'];
415
+ this.requestedWorkspaceId = requestedWorkspaceId;
333
416
  this.workspaceName = options.workspaceName;
417
+ if (options.workspaceName && !options.workspaceId) {
418
+ console.warn(
419
+ '[AgentRelay] workspaceName without workspaceId is deprecated and will be removed in a future major version. ' +
420
+ 'Set workspaceId explicitly to avoid silent behavior changes.'
421
+ );
422
+ }
334
423
  this.relaycastBaseUrl = options.relaycastBaseUrl;
335
424
  this.clientOptions = {
336
425
  binaryPath: options.binaryPath,
337
426
  binaryArgs: options.binaryArgs,
338
- brokerName: options.brokerName ?? options.workspaceName,
427
+ brokerName: options.brokerName ?? options.workspaceName ?? requestedWorkspaceId,
339
428
  channels: this.defaultChannels,
340
429
  cwd: options.cwd,
341
430
  env: options.env,
342
431
  requestTimeoutMs: options.requestTimeoutMs,
343
- shutdownTimeoutMs: options.shutdownTimeoutMs,
344
432
  };
345
433
 
346
434
  this.codex = this.createSpawner('codex', 'Codex', 'pty');
@@ -349,28 +437,122 @@ export class AgentRelay {
349
437
  this.opencode = this.createSpawner('opencode', 'OpenCode', 'headless');
350
438
  }
351
439
 
440
+ private getWorkspaceRegistryPath(): string {
441
+ return path.join(this.clientOptions.cwd ?? process.cwd(), '.relay', 'workspaces.json');
442
+ }
443
+
444
+ private readWorkspaceRegistry(): WorkspaceRegistry {
445
+ const registryPath = this.getWorkspaceRegistryPath();
446
+ if (!existsSync(registryPath)) {
447
+ return {};
448
+ }
449
+
450
+ let raw: string;
451
+ try {
452
+ raw = readFileSync(registryPath, 'utf8').trim();
453
+ } catch {
454
+ return {};
455
+ }
456
+ if (!raw) {
457
+ return {};
458
+ }
459
+
460
+ let parsed: unknown;
461
+ try {
462
+ parsed = JSON.parse(raw);
463
+ } catch {
464
+ // Registry file is corrupted (partial write, disk full, concurrent access).
465
+ // Return empty registry so callers can re-create it.
466
+ return {};
467
+ }
468
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
469
+ return {};
470
+ }
471
+
472
+ const registry: WorkspaceRegistry = {};
473
+ for (const [workspaceId, entry] of Object.entries(parsed as Record<string, unknown>)) {
474
+ const normalizedId = normalizeWorkspaceId(workspaceId);
475
+ if (!normalizedId) continue;
476
+ registry[normalizedId] = toWorkspaceRegistryEntry(entry);
477
+ }
478
+ return registry;
479
+ }
480
+
481
+ private writeWorkspaceRegistry(registry: WorkspaceRegistry): void {
482
+ const registryPath = this.getWorkspaceRegistryPath();
483
+ mkdirSync(path.dirname(registryPath), { recursive: true });
484
+ writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
485
+ }
486
+
487
+ private persistWorkspaceMapping(workspaceId: string, apiKey: string): void {
488
+ const registry = this.readWorkspaceRegistry();
489
+ const existing = registry[workspaceId] ?? {};
490
+ registry[workspaceId] = {
491
+ ...existing,
492
+ relaycastApiKey: apiKey,
493
+ relayfileUrl: existing.relayfileUrl,
494
+ createdAt: existing.createdAt ?? new Date().toISOString(),
495
+ agents: existing.agents ?? [],
496
+ };
497
+ this.writeWorkspaceRegistry(registry);
498
+ }
499
+
500
+ private findMappedWorkspaceIdByApiKey(apiKey: string): string | undefined {
501
+ const registry = this.readWorkspaceRegistry();
502
+ for (const [workspaceId, entry] of Object.entries(registry)) {
503
+ if (entry.relaycastApiKey === apiKey) {
504
+ return workspaceId;
505
+ }
506
+ }
507
+ return undefined;
508
+ }
509
+
510
+ private getResolvedWorkspaceId(): string | undefined {
511
+ return this.resolvedWorkspaceId ?? this.requestedWorkspaceId;
512
+ }
513
+
514
+ private getRelaycastBaseUrl(): string {
515
+ return (
516
+ this.relaycastBaseUrl ??
517
+ this.clientOptions.env?.RELAYCAST_BASE_URL ??
518
+ process.env.RELAYCAST_BASE_URL ??
519
+ 'https://api.relaycast.dev'
520
+ );
521
+ }
522
+
523
+ private applyWorkspaceEnv(workspaceId: string, apiKey: string): void {
524
+ const env: NodeJS.ProcessEnv = {
525
+ ...(this.clientOptions.env ?? process.env),
526
+ RELAY_API_KEY: apiKey,
527
+ RELAYFILE_WORKSPACE: workspaceId,
528
+ RELAY_DEFAULT_WORKSPACE: workspaceId,
529
+ RELAY_WORKSPACE_ID: workspaceId,
530
+ RELAY_WORKSPACES_JSON: JSON.stringify([{ workspace_id: workspaceId, api_key: apiKey }]),
531
+ };
532
+ if (this.relaycastBaseUrl) {
533
+ env.RELAYCAST_BASE_URL = this.relaycastBaseUrl;
534
+ }
535
+ this.clientOptions.env = env;
536
+ }
537
+
538
+ private async createMappedRelaycastWorkspace(workspaceId: string): Promise<string> {
539
+ const created = await RelayCast.createWorkspace(
540
+ this.workspaceName ?? workspaceId,
541
+ this.getRelaycastBaseUrl()
542
+ );
543
+ return created.apiKey;
544
+ }
545
+
352
546
  /**
353
547
  * Subscribe to broker stderr output. Listener is wired immediately if the
354
548
  * client is already started, otherwise it is attached when the client starts.
355
549
  * Returns an unsubscribe function.
356
550
  */
357
551
  onBrokerStderr(listener: (line: string) => void): () => void {
358
- if (this.client) {
359
- return this.client.onBrokerStderr(listener);
360
- }
361
- // Queue it: once ensureStarted completes, wire it up
362
- let unsub: (() => void) | undefined;
363
- const queuedUnsub = () => {
364
- unsub?.();
552
+ this.stderrListeners.add(listener);
553
+ return () => {
554
+ this.stderrListeners.delete(listener);
365
555
  };
366
- // Use the start promise if one is pending
367
- const promise = this.startPromise ?? this.ensureStarted();
368
- promise
369
- .then((c) => {
370
- unsub = c.onBrokerStderr(listener);
371
- })
372
- .catch(() => {});
373
- return queuedUnsub;
374
556
  }
375
557
 
376
558
  // ── Spawning ────────────────────────────────────────────────────────────
@@ -594,7 +776,7 @@ export class AgentRelay {
594
776
  /** Pre-register a batch of agents with Relaycast before steps execute. */
595
777
  async preflightAgents(agents: Array<{ name: string; cli: string }>): Promise<void> {
596
778
  const client = await this.ensureStarted();
597
- await client.preflightAgents(agents);
779
+ await client.preflight(agents);
598
780
  }
599
781
 
600
782
  /** List agents with PIDs from the broker (for worker registration). */
@@ -939,36 +1121,39 @@ export class AgentRelay {
939
1121
  */
940
1122
  private async ensureRelaycastApiKey(): Promise<void> {
941
1123
  if (this.relayApiKey) {
942
- this.wireRelaycastBaseUrl();
1124
+ const workspaceId = this.getResolvedWorkspaceId();
1125
+ if (workspaceId) {
1126
+ this.applyWorkspaceEnv(workspaceId, this.relayApiKey);
1127
+ try { this.persistWorkspaceMapping(workspaceId, this.relayApiKey); } catch { /* non-critical */ }
1128
+ } else {
1129
+ this.wireRelaycastBaseUrl();
1130
+ }
943
1131
  return;
944
1132
  }
945
1133
 
946
1134
  const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
947
- if (envKey) {
948
- this.relayApiKey = envKey;
949
- // Ensure the broker subprocess inherits the full process env + the key.
950
- // Without this, spawning with an explicit binaryPath but no env option
951
- // would cause the broker to start with an empty environment (no PATH,
952
- // no RELAY_API_KEY), making connect_relay() hang and triggering the
953
- // hello-handshake timeout.
954
- if (!this.clientOptions.env) {
955
- this.clientOptions.env = { ...process.env, RELAY_API_KEY: envKey };
956
- } else if (!this.clientOptions.env.RELAY_API_KEY) {
957
- this.clientOptions.env.RELAY_API_KEY = envKey;
958
- }
959
- this.wireRelaycastBaseUrl();
1135
+ const requestedWorkspaceId = this.requestedWorkspaceId;
1136
+ if (requestedWorkspaceId) {
1137
+ const registry = this.readWorkspaceRegistry();
1138
+ const mappedKey = registry[requestedWorkspaceId]?.relaycastApiKey;
1139
+ const resolvedKey =
1140
+ mappedKey ?? envKey ?? (await this.createMappedRelaycastWorkspace(requestedWorkspaceId));
1141
+ this.relayApiKey = resolvedKey;
1142
+ this.resolvedWorkspaceId = requestedWorkspaceId;
1143
+ this.applyWorkspaceEnv(requestedWorkspaceId, resolvedKey);
1144
+ try { this.persistWorkspaceMapping(requestedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
960
1145
  return;
961
1146
  }
962
1147
 
963
- // No API key in env — broker will create/select its own workspace.
964
- // Ensure the broker process inherits the full environment (PATH, etc.)
965
- // so it can connect to Relaycast. The actual workspace key will be
966
- // read from the broker's hello_ack response in ensureStarted().
967
- if (!this.clientOptions.env) {
968
- this.clientOptions.env = { ...process.env };
969
- }
1148
+ const resolvedWorkspaceId = envKey
1149
+ ? (this.findMappedWorkspaceIdByApiKey(envKey) ?? generateWorkspaceId())
1150
+ : generateWorkspaceId();
1151
+ const resolvedKey = envKey ?? (await this.createMappedRelaycastWorkspace(resolvedWorkspaceId));
970
1152
 
971
- this.wireRelaycastBaseUrl();
1153
+ this.relayApiKey = resolvedKey;
1154
+ this.resolvedWorkspaceId = resolvedWorkspaceId;
1155
+ this.applyWorkspaceEnv(resolvedWorkspaceId, resolvedKey);
1156
+ try { this.persistWorkspaceMapping(resolvedWorkspaceId, resolvedKey); } catch { /* non-critical */ }
972
1157
  }
973
1158
 
974
1159
  /** Inject relaycastBaseUrl into broker env. Explicit option wins over inherited env. */
@@ -983,19 +1168,38 @@ export class AgentRelay {
983
1168
  if (this.startPromise) return this.startPromise;
984
1169
 
985
1170
  this.startPromise = this.ensureRelaycastApiKey()
986
- .then(() => AgentRelayClient.start(this.clientOptions))
1171
+ .then(() =>
1172
+ AgentRelayClient.spawn({
1173
+ ...this.clientOptions,
1174
+ onStderr: (line) => {
1175
+ for (const listener of this.stderrListeners) {
1176
+ try {
1177
+ listener(line);
1178
+ } catch {
1179
+ /* ignore */
1180
+ }
1181
+ }
1182
+ },
1183
+ })
1184
+ )
987
1185
  .then((c) => {
988
- this.client = c;
989
- this.startPromise = undefined;
990
1186
  // Use the workspace key the broker actually connected with.
991
1187
  // This ensures SDK and workers are always on the same workspace.
992
1188
  if (c.workspaceKey) {
993
1189
  this.relayApiKey = c.workspaceKey;
1190
+ const workspaceId = this.getResolvedWorkspaceId();
1191
+ if (workspaceId) {
1192
+ this.applyWorkspaceEnv(workspaceId, c.workspaceKey);
1193
+ try { this.persistWorkspaceMapping(workspaceId, c.workspaceKey); } catch { /* non-critical */ }
1194
+ }
994
1195
  }
995
1196
  this.wireEvents(c);
1197
+ this.client = c;
1198
+ this.startPromise = undefined;
996
1199
  return c;
997
1200
  })
998
1201
  .catch((err) => {
1202
+ this.client = undefined;
999
1203
  this.startPromise = undefined;
1000
1204
  throw err;
1001
1205
  });
@@ -1332,7 +1536,10 @@ export class AgentRelay {
1332
1536
  async unsubscribe(channelsToRemove: string[]) {
1333
1537
  await relay.unsubscribe({ agent: name, channels: channelsToRemove });
1334
1538
  },
1335
- onOutput(callback: AgentOutputCallback, options?: { stream?: string; mode?: 'chunk' | 'structured' }): () => void {
1539
+ onOutput(
1540
+ callback: AgentOutputCallback,
1541
+ options?: { stream?: string; mode?: 'chunk' | 'structured' }
1542
+ ): () => void {
1336
1543
  let listeners = relay.outputListeners.get(name);
1337
1544
  if (!listeners) {
1338
1545
  listeners = new Set();
@@ -0,0 +1,216 @@
1
+ /**
2
+ * BrokerTransport — HTTP/WS transport layer for communicating with the
3
+ * agent-relay broker. Used internally by AgentRelayClient.
4
+ *
5
+ * Handles:
6
+ * - HTTP requests with API key auth and structured error parsing
7
+ * - WebSocket connection for real-time event streaming
8
+ * - Event buffering, replay, and query (mirrors stdio client behavior)
9
+ */
10
+
11
+ import WebSocket from 'ws';
12
+ import type { BrokerEvent } from './protocol.js';
13
+
14
+ export class AgentRelayProtocolError extends Error {
15
+ code: string;
16
+ retryable: boolean;
17
+ data?: unknown;
18
+
19
+ constructor(payload: { code: string; message: string; retryable?: boolean; data?: unknown }) {
20
+ super(payload.message);
21
+ this.name = 'AgentRelayProtocolError';
22
+ this.code = payload.code;
23
+ this.retryable = payload.retryable ?? false;
24
+ this.data = payload.data;
25
+ }
26
+ }
27
+
28
+ export interface BrokerTransportOptions {
29
+ baseUrl: string;
30
+ apiKey?: string;
31
+ /** Timeout in ms for HTTP requests. Default: 30000. */
32
+ requestTimeoutMs?: number;
33
+ /** Maximum number of events to buffer in memory for queryEvents/getLastEvent */
34
+ maxBufferSize?: number;
35
+ }
36
+
37
+ export class BrokerTransport {
38
+ private readonly baseUrl: string;
39
+ private readonly apiKey?: string;
40
+ private readonly requestTimeoutMs: number;
41
+ private readonly maxBufferSize: number;
42
+
43
+ private ws: WebSocket | null = null;
44
+ private eventListeners = new Set<(event: BrokerEvent) => void>();
45
+ private eventBuffer: BrokerEvent[] = [];
46
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
47
+ private sinceSeq = 0;
48
+ private _connected = false;
49
+ private _intentionalClose = false;
50
+
51
+ constructor(options: BrokerTransportOptions) {
52
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
53
+ this.apiKey = options.apiKey;
54
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
55
+ this.maxBufferSize = options.maxBufferSize ?? 1000;
56
+ }
57
+
58
+ get connected(): boolean {
59
+ return this._connected;
60
+ }
61
+
62
+ get wsUrl(): string {
63
+ return this.baseUrl.replace(/^http/, 'ws') + '/ws';
64
+ }
65
+
66
+ // ── HTTP ─────────────────────────────────────────────────────────────
67
+
68
+ async request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
69
+ const headers = new Headers(init?.headers);
70
+ if (!headers.has('Content-Type')) {
71
+ headers.set('Content-Type', 'application/json');
72
+ }
73
+ if (this.apiKey) {
74
+ headers.set('X-API-Key', this.apiKey);
75
+ }
76
+
77
+ const signal = init?.signal ?? AbortSignal.timeout(this.requestTimeoutMs);
78
+ const res = await fetch(`${this.baseUrl}${path}`, { ...init, headers, signal });
79
+
80
+ if (!res.ok) {
81
+ let body: { code?: string; message?: string; error?: string } | undefined;
82
+ try {
83
+ body = (await res.json()) as { code?: string; message?: string; error?: string };
84
+ } catch {
85
+ // non-JSON error
86
+ }
87
+ throw new AgentRelayProtocolError({
88
+ code: body?.code ?? `http_${res.status}`,
89
+ message: body?.message ?? body?.error ?? res.statusText,
90
+ retryable: res.status >= 500,
91
+ });
92
+ }
93
+
94
+ return res.json() as Promise<T>;
95
+ }
96
+
97
+ // ── WebSocket events ─────────────────────────────────────────────────
98
+
99
+ connect(sinceSeq?: number): void {
100
+ if (this.ws) return;
101
+ // Clear any pending reconnect timer to avoid duplicate connections
102
+ if (this.reconnectTimer) {
103
+ clearTimeout(this.reconnectTimer);
104
+ this.reconnectTimer = null;
105
+ }
106
+ this.sinceSeq = sinceSeq ?? this.sinceSeq;
107
+ this._connect();
108
+ }
109
+
110
+ private _connect(): void {
111
+ this._intentionalClose = false;
112
+ const url = `${this.wsUrl}?sinceSeq=${this.sinceSeq}`;
113
+ const headers: Record<string, string> = {};
114
+ if (this.apiKey) {
115
+ headers['X-API-Key'] = this.apiKey;
116
+ }
117
+
118
+ this.ws = new WebSocket(url, { headers });
119
+
120
+ this.ws.on('open', () => {
121
+ this._connected = true;
122
+ if (this.reconnectTimer) {
123
+ clearTimeout(this.reconnectTimer);
124
+ this.reconnectTimer = null;
125
+ }
126
+ });
127
+
128
+ this.ws.on('message', (data) => {
129
+ try {
130
+ const event = JSON.parse(data.toString()) as BrokerEvent & { seq?: number };
131
+ // Track sequence for replay on reconnect
132
+ if (typeof event.seq === 'number' && event.seq > this.sinceSeq) {
133
+ this.sinceSeq = event.seq;
134
+ }
135
+ // Buffer the event
136
+ this.eventBuffer.push(event);
137
+ if (this.eventBuffer.length > this.maxBufferSize) {
138
+ this.eventBuffer = this.eventBuffer.slice(-this.maxBufferSize);
139
+ }
140
+ // Notify listeners
141
+ for (const listener of this.eventListeners) {
142
+ try {
143
+ listener(event);
144
+ } catch {
145
+ // don't let a bad listener break the stream
146
+ }
147
+ }
148
+ } catch {
149
+ // ignore non-JSON frames (ping/pong)
150
+ }
151
+ });
152
+
153
+ this.ws.on('close', () => {
154
+ this._connected = false;
155
+ this.ws = null;
156
+ // Auto-reconnect after 2s unless intentionally closed
157
+ if (!this._intentionalClose) {
158
+ this.reconnectTimer = setTimeout(() => this._connect(), 2000);
159
+ }
160
+ });
161
+
162
+ this.ws.on('error', () => {
163
+ // error always followed by close
164
+ });
165
+ }
166
+
167
+ disconnect(): void {
168
+ this._intentionalClose = true;
169
+ if (this.reconnectTimer) {
170
+ clearTimeout(this.reconnectTimer);
171
+ this.reconnectTimer = null;
172
+ }
173
+ if (this.ws) {
174
+ this.ws.close();
175
+ this.ws = null;
176
+ }
177
+ this._connected = false;
178
+ }
179
+
180
+ onEvent(listener: (event: BrokerEvent) => void): () => void {
181
+ this.eventListeners.add(listener);
182
+ return () => {
183
+ this.eventListeners.delete(listener);
184
+ };
185
+ }
186
+
187
+ queryEvents(filter?: { kind?: string; name?: string; since?: number; limit?: number }): BrokerEvent[] {
188
+ let events = [...this.eventBuffer];
189
+ if (filter?.kind) {
190
+ events = events.filter((e) => e.kind === filter.kind);
191
+ }
192
+ if (filter?.name) {
193
+ events = events.filter((e) => 'name' in e && e.name === filter.name);
194
+ }
195
+ if (filter?.since !== undefined) {
196
+ const since = filter.since;
197
+ events = events.filter(
198
+ (e) => 'timestamp' in e && typeof e.timestamp === 'number' && e.timestamp >= since
199
+ );
200
+ }
201
+ if (filter?.limit !== undefined) {
202
+ events = events.slice(-filter.limit);
203
+ }
204
+ return events;
205
+ }
206
+
207
+ getLastEvent(kind: string, name?: string): BrokerEvent | undefined {
208
+ for (let i = this.eventBuffer.length - 1; i >= 0; i -= 1) {
209
+ const event = this.eventBuffer[i];
210
+ if (event.kind === kind && (!name || ('name' in event && event.name === name))) {
211
+ return event;
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+ }