agent-relay 2.1.4 → 2.1.6

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 (132) hide show
  1. package/README.md +85 -236
  2. package/dist/index.cjs +281 -24
  3. package/package.json +19 -19
  4. package/packages/api-types/package.json +1 -1
  5. package/packages/benchmark/package.json +4 -4
  6. package/packages/bridge/dist/spawner.d.ts.map +1 -1
  7. package/packages/bridge/dist/spawner.js +39 -5
  8. package/packages/bridge/dist/spawner.js.map +1 -1
  9. package/packages/bridge/package.json +8 -8
  10. package/packages/bridge/src/spawner.ts +40 -5
  11. package/packages/cli-tester/package.json +1 -1
  12. package/packages/config/package.json +2 -2
  13. package/packages/continuity/package.json +2 -2
  14. package/packages/daemon/dist/server.d.ts +5 -0
  15. package/packages/daemon/dist/server.d.ts.map +1 -1
  16. package/packages/daemon/dist/server.js +31 -0
  17. package/packages/daemon/dist/server.js.map +1 -1
  18. package/packages/daemon/package.json +12 -12
  19. package/packages/daemon/src/server.ts +37 -0
  20. package/packages/hooks/package.json +4 -4
  21. package/packages/mcp/dist/cloud.d.ts +7 -114
  22. package/packages/mcp/dist/cloud.d.ts.map +1 -1
  23. package/packages/mcp/dist/cloud.js +21 -431
  24. package/packages/mcp/dist/cloud.js.map +1 -1
  25. package/packages/mcp/dist/errors.d.ts +4 -22
  26. package/packages/mcp/dist/errors.d.ts.map +1 -1
  27. package/packages/mcp/dist/errors.js +4 -43
  28. package/packages/mcp/dist/errors.js.map +1 -1
  29. package/packages/mcp/dist/hybrid-client.d.ts.map +1 -1
  30. package/packages/mcp/dist/hybrid-client.js +7 -1
  31. package/packages/mcp/dist/hybrid-client.js.map +1 -1
  32. package/packages/mcp/package.json +4 -3
  33. package/packages/mcp/src/cloud.ts +29 -511
  34. package/packages/mcp/src/errors.ts +12 -49
  35. package/packages/mcp/src/hybrid-client.ts +8 -1
  36. package/packages/mcp/tests/discover.test.ts +72 -11
  37. package/packages/memory/package.json +2 -2
  38. package/packages/policy/package.json +2 -2
  39. package/packages/protocol/dist/types.d.ts +17 -1
  40. package/packages/protocol/dist/types.d.ts.map +1 -1
  41. package/packages/protocol/package.json +1 -1
  42. package/packages/protocol/src/types.ts +23 -0
  43. package/packages/resiliency/package.json +1 -1
  44. package/packages/sdk/dist/browser-client.d.ts +212 -0
  45. package/packages/sdk/dist/browser-client.d.ts.map +1 -0
  46. package/packages/sdk/dist/browser-client.js +750 -0
  47. package/packages/sdk/dist/browser-client.js.map +1 -0
  48. package/packages/sdk/dist/browser-framing.d.ts +46 -0
  49. package/packages/sdk/dist/browser-framing.d.ts.map +1 -0
  50. package/packages/sdk/dist/browser-framing.js +122 -0
  51. package/packages/sdk/dist/browser-framing.js.map +1 -0
  52. package/packages/sdk/dist/client.d.ts +129 -2
  53. package/packages/sdk/dist/client.d.ts.map +1 -1
  54. package/packages/sdk/dist/client.js +312 -2
  55. package/packages/sdk/dist/client.js.map +1 -1
  56. package/packages/sdk/dist/discovery.d.ts +10 -0
  57. package/packages/sdk/dist/discovery.d.ts.map +1 -0
  58. package/packages/sdk/dist/discovery.js +22 -0
  59. package/packages/sdk/dist/discovery.js.map +1 -0
  60. package/packages/sdk/dist/errors.d.ts +9 -0
  61. package/packages/sdk/dist/errors.d.ts.map +1 -0
  62. package/packages/sdk/dist/errors.js +9 -0
  63. package/packages/sdk/dist/errors.js.map +1 -0
  64. package/packages/sdk/dist/index.d.ts +18 -2
  65. package/packages/sdk/dist/index.d.ts.map +1 -1
  66. package/packages/sdk/dist/index.js +27 -1
  67. package/packages/sdk/dist/index.js.map +1 -1
  68. package/packages/sdk/dist/transports/index.d.ts +92 -0
  69. package/packages/sdk/dist/transports/index.d.ts.map +1 -0
  70. package/packages/sdk/dist/transports/index.js +129 -0
  71. package/packages/sdk/dist/transports/index.js.map +1 -0
  72. package/packages/sdk/dist/transports/socket-transport.d.ts +30 -0
  73. package/packages/sdk/dist/transports/socket-transport.d.ts.map +1 -0
  74. package/packages/sdk/dist/transports/socket-transport.js +94 -0
  75. package/packages/sdk/dist/transports/socket-transport.js.map +1 -0
  76. package/packages/sdk/dist/transports/types.d.ts +69 -0
  77. package/packages/sdk/dist/transports/types.d.ts.map +1 -0
  78. package/packages/sdk/dist/transports/types.js +10 -0
  79. package/packages/sdk/dist/transports/types.js.map +1 -0
  80. package/packages/sdk/dist/transports/websocket-transport.d.ts +55 -0
  81. package/packages/sdk/dist/transports/websocket-transport.d.ts.map +1 -0
  82. package/packages/sdk/dist/transports/websocket-transport.js +180 -0
  83. package/packages/sdk/dist/transports/websocket-transport.js.map +1 -0
  84. package/packages/sdk/package.json +28 -4
  85. package/packages/sdk/src/browser-client.ts +985 -0
  86. package/packages/sdk/src/browser-framing.test.ts +115 -0
  87. package/packages/sdk/src/browser-framing.ts +150 -0
  88. package/packages/sdk/src/client.test.ts +425 -0
  89. package/packages/sdk/src/client.ts +397 -3
  90. package/packages/sdk/src/discovery.ts +38 -0
  91. package/packages/sdk/src/errors.ts +17 -0
  92. package/packages/sdk/src/index.ts +82 -1
  93. package/packages/sdk/src/transports/index.ts +197 -0
  94. package/packages/sdk/src/transports/socket-transport.ts +115 -0
  95. package/packages/sdk/src/transports/types.ts +77 -0
  96. package/packages/sdk/src/transports/websocket-transport.ts +245 -0
  97. package/packages/sdk/tsconfig.json +1 -1
  98. package/packages/spawner/package.json +1 -1
  99. package/packages/state/package.json +1 -1
  100. package/packages/storage/package.json +2 -2
  101. package/packages/storage/src/jsonl-adapter.test.ts +8 -3
  102. package/packages/telemetry/package.json +1 -1
  103. package/packages/trajectory/package.json +2 -2
  104. package/packages/user-directory/package.json +2 -2
  105. package/packages/utils/dist/cjs/discovery.js +328 -0
  106. package/packages/utils/dist/cjs/errors.js +81 -0
  107. package/packages/utils/dist/discovery.d.ts +123 -0
  108. package/packages/utils/dist/discovery.d.ts.map +1 -0
  109. package/packages/utils/dist/discovery.js +439 -0
  110. package/packages/utils/dist/discovery.js.map +1 -0
  111. package/packages/utils/dist/errors.d.ts +29 -0
  112. package/packages/utils/dist/errors.d.ts.map +1 -0
  113. package/packages/utils/dist/errors.js +50 -0
  114. package/packages/utils/dist/errors.js.map +1 -0
  115. package/packages/utils/package.json +15 -2
  116. package/packages/utils/src/consolidation.test.ts +125 -0
  117. package/packages/utils/src/discovery.test.ts +196 -0
  118. package/packages/utils/src/discovery.ts +524 -0
  119. package/packages/utils/src/errors.test.ts +83 -0
  120. package/packages/utils/src/errors.ts +56 -0
  121. package/packages/wrapper/dist/opencode-wrapper.d.ts +6 -2
  122. package/packages/wrapper/dist/opencode-wrapper.d.ts.map +1 -1
  123. package/packages/wrapper/dist/opencode-wrapper.js +34 -10
  124. package/packages/wrapper/dist/opencode-wrapper.js.map +1 -1
  125. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +22 -2
  126. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -1
  127. package/packages/wrapper/dist/relay-pty-orchestrator.js +174 -4
  128. package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -1
  129. package/packages/wrapper/package.json +6 -6
  130. package/packages/wrapper/src/opencode-wrapper.ts +37 -9
  131. package/packages/wrapper/src/relay-pty-orchestrator.ts +197 -4
  132. package/relay-snippets/agent-relay-snippet.md +17 -5
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Socket Discovery & Cloud Workspace Detection
3
+ *
4
+ * Single source of truth for discovering relay daemon sockets,
5
+ * cloud workspace environments, and agent identity.
6
+ *
7
+ * Previously duplicated in @agent-relay/mcp (cloud.ts). Now consolidated
8
+ * here in the SDK so both SDK and MCP use the same discovery logic.
9
+ */
10
+
11
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { findProjectRoot } from '@agent-relay/config';
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export interface CloudWorkspace {
21
+ workspaceId: string;
22
+ cloudApiUrl: string;
23
+ workspaceToken?: string;
24
+ ownerUserId?: string;
25
+ }
26
+
27
+ export interface DiscoveryResult {
28
+ socketPath: string;
29
+ project: string;
30
+ source: 'env' | 'cloud' | 'cwd' | 'scan';
31
+ isCloud: boolean;
32
+ workspace?: CloudWorkspace;
33
+ }
34
+
35
+ export interface CloudConnectionOptions {
36
+ /** Override socket path (for testing) */
37
+ socketPath?: string;
38
+ /** Override workspace detection */
39
+ workspace?: Partial<CloudWorkspace>;
40
+ }
41
+
42
+ export interface CloudConnectionInfo {
43
+ socketPath: string;
44
+ project: string;
45
+ isCloud: boolean;
46
+ workspace?: CloudWorkspace;
47
+ daemonUrl?: string;
48
+ }
49
+
50
+ // ============================================================================
51
+ // Cloud Workspace Detection
52
+ // ============================================================================
53
+
54
+ /**
55
+ * Detect if running in a cloud workspace environment.
56
+ *
57
+ * Cloud workspaces set these environment variables:
58
+ * - WORKSPACE_ID: The unique workspace identifier
59
+ * - CLOUD_API_URL: The cloud API endpoint
60
+ * - WORKSPACE_TOKEN: Bearer token for API auth (optional)
61
+ * - WORKSPACE_OWNER_USER_ID: The workspace owner's user ID (optional)
62
+ */
63
+ export function detectCloudWorkspace(): CloudWorkspace | null {
64
+ const workspaceId = process.env.WORKSPACE_ID;
65
+ const cloudApiUrl = process.env.CLOUD_API_URL;
66
+
67
+ if (!workspaceId || !cloudApiUrl) {
68
+ return null;
69
+ }
70
+
71
+ return {
72
+ workspaceId,
73
+ cloudApiUrl,
74
+ workspaceToken: process.env.WORKSPACE_TOKEN,
75
+ ownerUserId: process.env.WORKSPACE_OWNER_USER_ID,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Check if we're running in a cloud workspace.
81
+ */
82
+ export function isCloudWorkspace(): boolean {
83
+ return detectCloudWorkspace() !== null;
84
+ }
85
+
86
+ // ============================================================================
87
+ // Workspace-Aware Socket Discovery
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Get the workspace-namespaced socket path.
92
+ *
93
+ * In cloud workspaces, sockets are stored at:
94
+ * /tmp/relay/{WORKSPACE_ID}/sockets/daemon.sock
95
+ *
96
+ * This provides multi-tenant isolation on shared infrastructure.
97
+ */
98
+ export function getCloudSocketPath(workspaceId: string): string {
99
+ return `/tmp/relay/${workspaceId}/sockets/daemon.sock`;
100
+ }
101
+
102
+ /**
103
+ * Get the workspace-namespaced outbox path.
104
+ *
105
+ * In cloud workspaces, outbox directories are at:
106
+ * /tmp/relay/{WORKSPACE_ID}/outbox/{agentName}/
107
+ */
108
+ export function getCloudOutboxPath(workspaceId: string, agentName: string): string {
109
+ return `/tmp/relay/${workspaceId}/outbox/${agentName}`;
110
+ }
111
+
112
+ /**
113
+ * Get platform-specific data directory.
114
+ */
115
+ function getDataDir(): string {
116
+ const platform = process.platform;
117
+
118
+ if (platform === 'darwin') {
119
+ return join(homedir(), 'Library', 'Application Support', 'agent-relay');
120
+ } else if (platform === 'win32') {
121
+ return join(process.env.APPDATA || homedir(), 'agent-relay');
122
+ } else {
123
+ return join(
124
+ process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'),
125
+ 'agent-relay'
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Discover relay daemon socket with cloud-awareness.
132
+ *
133
+ * Priority order:
134
+ * 1. RELAY_SOCKET environment variable (explicit path)
135
+ * 2. Cloud workspace socket (if WORKSPACE_ID is set)
136
+ * 3. RELAY_PROJECT environment variable (project name -> data dir)
137
+ * 4. Current working directory .relay/config.json
138
+ * 5. Scan data directory for active sockets
139
+ *
140
+ * @param options - Optional configuration overrides
141
+ * @returns Discovery result with socket path, project info, and cloud status
142
+ */
143
+ export function discoverSocket(options: CloudConnectionOptions = {}): DiscoveryResult | null {
144
+ // 0. Use override if provided
145
+ if (options.socketPath) {
146
+ const workspace = options.workspace
147
+ ? ({
148
+ workspaceId: options.workspace.workspaceId || 'override',
149
+ cloudApiUrl: options.workspace.cloudApiUrl || '',
150
+ } as CloudWorkspace)
151
+ : undefined;
152
+
153
+ return {
154
+ socketPath: options.socketPath,
155
+ project: workspace?.workspaceId || 'override',
156
+ source: 'env',
157
+ isCloud: !!workspace,
158
+ workspace,
159
+ };
160
+ }
161
+
162
+ // 1. Explicit socket path from environment
163
+ const socketEnv = process.env.RELAY_SOCKET;
164
+ if (socketEnv) {
165
+ const workspace = detectCloudWorkspace();
166
+ return {
167
+ socketPath: socketEnv,
168
+ project: process.env.RELAY_PROJECT || workspace?.workspaceId || 'unknown',
169
+ source: 'env',
170
+ isCloud: !!workspace,
171
+ workspace: workspace || undefined,
172
+ };
173
+ }
174
+
175
+ // 2. Cloud workspace socket (highest priority for cloud environments)
176
+ // Return the determined path even if the socket file doesn't exist yet
177
+ // (daemon may not have started)
178
+ const workspace = detectCloudWorkspace();
179
+ if (workspace) {
180
+ const cloudSocket = getCloudSocketPath(workspace.workspaceId);
181
+ return {
182
+ socketPath: cloudSocket,
183
+ project: workspace.workspaceId,
184
+ source: 'cloud',
185
+ isCloud: true,
186
+ workspace,
187
+ };
188
+ }
189
+
190
+ // 3. Project name -> data dir lookup
191
+ const projectEnv = process.env.RELAY_PROJECT;
192
+ if (projectEnv) {
193
+ const dataDir = getDataDir();
194
+ const projectSocket = join(dataDir, 'projects', projectEnv, 'daemon.sock');
195
+ return {
196
+ socketPath: projectSocket,
197
+ project: projectEnv,
198
+ source: 'env',
199
+ isCloud: false,
200
+ };
201
+ }
202
+
203
+ // 4. Project-local socket (created by daemon in project's .agent-relay directory)
204
+ // This is the primary path for local development
205
+ // First try cwd, then scan up to find project root
206
+ const projectRoot = findProjectRoot(process.cwd());
207
+ const searchDirs = [process.cwd()];
208
+ if (projectRoot && projectRoot !== process.cwd()) {
209
+ searchDirs.push(projectRoot);
210
+ }
211
+
212
+ for (const dir of searchDirs) {
213
+ const projectLocalSocket = join(dir, '.agent-relay', 'relay.sock');
214
+ if (existsSync(projectLocalSocket)) {
215
+ // Read project ID from marker file if available
216
+ let projectId = 'local';
217
+ const markerPath = join(dir, '.agent-relay', '.project');
218
+ if (existsSync(markerPath)) {
219
+ try {
220
+ const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
221
+ projectId = marker.projectId || 'local';
222
+ } catch {
223
+ // Ignore marker read errors
224
+ }
225
+ }
226
+ return {
227
+ socketPath: projectLocalSocket,
228
+ project: projectId,
229
+ source: 'cwd',
230
+ isCloud: false,
231
+ };
232
+ }
233
+ }
234
+
235
+ // 4b. Legacy .relay/config.json support
236
+ const cwdConfig = join(process.cwd(), '.relay', 'config.json');
237
+ if (existsSync(cwdConfig)) {
238
+ try {
239
+ const config = JSON.parse(readFileSync(cwdConfig, 'utf-8'));
240
+ if (config.socketPath) {
241
+ return {
242
+ socketPath: config.socketPath,
243
+ project: config.project || 'local',
244
+ source: 'cwd',
245
+ isCloud: false,
246
+ };
247
+ }
248
+ } catch (err) {
249
+ // Invalid config (malformed JSON, permission error, etc.), continue to next method
250
+ if (process.env.DEBUG || process.env.RELAY_DEBUG) {
251
+ console.debug('[discovery] Failed to read cwd config:', cwdConfig, err);
252
+ }
253
+ }
254
+ }
255
+
256
+ // 5. Scan data directory for active sockets
257
+ const dataDir = getDataDir();
258
+ const projectsDir = join(dataDir, 'projects');
259
+
260
+ if (existsSync(projectsDir)) {
261
+ try {
262
+ const projects = readdirSync(projectsDir, { withFileTypes: true })
263
+ .filter((d) => d.isDirectory())
264
+ .map((d) => d.name);
265
+
266
+ for (const project of projects) {
267
+ const socketPath = join(projectsDir, project, 'daemon.sock');
268
+ if (existsSync(socketPath)) {
269
+ return {
270
+ socketPath,
271
+ project,
272
+ source: 'scan',
273
+ isCloud: false,
274
+ };
275
+ }
276
+ }
277
+ } catch (err) {
278
+ // Directory read failed (permission error, etc.), return null
279
+ if (process.env.DEBUG || process.env.RELAY_DEBUG) {
280
+ console.debug('[discovery] Failed to scan projects directory:', projectsDir, err);
281
+ }
282
+ }
283
+ }
284
+
285
+ return null;
286
+ }
287
+
288
+ // ============================================================================
289
+ // Cloud API Helpers
290
+ // ============================================================================
291
+
292
+ /**
293
+ * Make an authenticated request to the cloud API.
294
+ *
295
+ * @param workspace - Cloud workspace configuration
296
+ * @param path - API path (e.g., '/api/status')
297
+ * @param options - Fetch options
298
+ * @returns Response from the API
299
+ */
300
+ export async function cloudApiRequest(
301
+ workspace: CloudWorkspace,
302
+ path: string,
303
+ options: RequestInit = {}
304
+ ): Promise<Response> {
305
+ const url = `${workspace.cloudApiUrl}${path}`;
306
+
307
+ const headers: Record<string, string> = {
308
+ 'Content-Type': 'application/json',
309
+ ...(options.headers as Record<string, string>),
310
+ };
311
+
312
+ if (workspace.workspaceToken) {
313
+ headers['Authorization'] = `Bearer ${workspace.workspaceToken}`;
314
+ }
315
+
316
+ return fetch(url, {
317
+ ...options,
318
+ headers,
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Get the workspace status from the cloud API.
324
+ */
325
+ export async function getWorkspaceStatus(
326
+ workspace: CloudWorkspace
327
+ ): Promise<{ status: string; agents?: string[] } | null> {
328
+ try {
329
+ const response = await cloudApiRequest(
330
+ workspace,
331
+ `/api/workspaces/${workspace.workspaceId}/status`
332
+ );
333
+
334
+ if (!response.ok) {
335
+ return null;
336
+ }
337
+
338
+ return (await response.json()) as { status: string; agents?: string[] };
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+
344
+ // ============================================================================
345
+ // Cloud Connection Factory
346
+ // ============================================================================
347
+
348
+ /**
349
+ * Get connection info for the relay daemon.
350
+ *
351
+ * This function determines the best way to connect to the daemon:
352
+ * - In cloud environments: Uses workspace-namespaced socket
353
+ * - In local environments: Uses standard socket discovery
354
+ *
355
+ * @param options - Optional configuration overrides
356
+ * @returns Connection info or null if daemon not found
357
+ */
358
+ export function getConnectionInfo(
359
+ options: CloudConnectionOptions = {}
360
+ ): CloudConnectionInfo | null {
361
+ const discovery = discoverSocket(options);
362
+
363
+ if (!discovery) {
364
+ return null;
365
+ }
366
+
367
+ const info: CloudConnectionInfo = {
368
+ socketPath: discovery.socketPath,
369
+ project: discovery.project,
370
+ isCloud: discovery.isCloud,
371
+ workspace: discovery.workspace,
372
+ };
373
+
374
+ // In cloud environments, we may also have a daemon URL for HTTP API access
375
+ if (discovery.workspace?.cloudApiUrl) {
376
+ info.daemonUrl = discovery.workspace.cloudApiUrl;
377
+ }
378
+
379
+ return info;
380
+ }
381
+
382
+ /**
383
+ * Environment variable summary for debugging.
384
+ */
385
+ export function getCloudEnvironmentSummary(): Record<string, string | undefined> {
386
+ return {
387
+ WORKSPACE_ID: process.env.WORKSPACE_ID,
388
+ CLOUD_API_URL: process.env.CLOUD_API_URL,
389
+ WORKSPACE_TOKEN: process.env.WORKSPACE_TOKEN ? '[set]' : undefined,
390
+ WORKSPACE_OWNER_USER_ID: process.env.WORKSPACE_OWNER_USER_ID,
391
+ RELAY_SOCKET: process.env.RELAY_SOCKET,
392
+ RELAY_PROJECT: process.env.RELAY_PROJECT,
393
+ RELAY_AGENT_NAME: process.env.RELAY_AGENT_NAME,
394
+ };
395
+ }
396
+
397
+ // ============================================================================
398
+ // Agent Identity Discovery
399
+ // ============================================================================
400
+
401
+ /**
402
+ * Discover the agent name for the MCP server.
403
+ *
404
+ * Priority order:
405
+ * 1. RELAY_AGENT_NAME environment variable (explicit)
406
+ * 2. Identity file in .agent-relay directory (written by wrapper)
407
+ * 3. Scan outbox directories to find agent's outbox
408
+ *
409
+ * @param _discovery - Optional discovery result (reserved for future use)
410
+ * @returns Agent name or null if not found
411
+ */
412
+ export function discoverAgentName(_discovery?: DiscoveryResult | null): string | null {
413
+ // 1. Explicit environment variable
414
+ const envName = process.env.RELAY_AGENT_NAME;
415
+ if (envName) {
416
+ return envName;
417
+ }
418
+
419
+ // 2. Identity file in .agent-relay directory
420
+ // The wrapper creates this file with the agent name
421
+ const projectRoot = findProjectRoot(process.cwd());
422
+ const searchDirs = [process.cwd()];
423
+ if (projectRoot && projectRoot !== process.cwd()) {
424
+ searchDirs.push(projectRoot);
425
+ }
426
+
427
+ for (const dir of searchDirs) {
428
+ const relayDir = join(dir, '.agent-relay');
429
+ if (!existsSync(relayDir)) continue;
430
+
431
+ // First check for per-process identity files
432
+ // The orchestrator writes mcp-identity-{orchestrator.pid}
433
+ // Try to find one by checking process.ppid and its ancestors
434
+ const pidIdentityPath = join(relayDir, `mcp-identity-${process.ppid}`);
435
+ if (existsSync(pidIdentityPath)) {
436
+ try {
437
+ const content = readFileSync(pidIdentityPath, 'utf-8').trim();
438
+ if (content) {
439
+ return content;
440
+ }
441
+ } catch {
442
+ // Ignore read errors
443
+ }
444
+ }
445
+
446
+ // Scan all mcp-identity-* files and return the most recently modified one
447
+ // This handles the case where MCP server's ppid doesn't match the orchestrator
448
+ try {
449
+ const files = readdirSync(relayDir, { withFileTypes: true })
450
+ .filter((d) => d.isFile() && d.name.startsWith('mcp-identity-'))
451
+ .map((d) => ({
452
+ path: join(relayDir, d.name),
453
+ name: d.name,
454
+ }));
455
+
456
+ if (files.length > 0) {
457
+ // Sort by mtime (most recent first) to get the latest identity
458
+ const sorted = files
459
+ .map((f) => {
460
+ try {
461
+ const stat = statSync(f.path);
462
+ return { ...f, mtime: stat.mtimeMs };
463
+ } catch {
464
+ return { ...f, mtime: 0 };
465
+ }
466
+ })
467
+ .sort((a, b) => b.mtime - a.mtime);
468
+
469
+ // Return the most recently modified identity file
470
+ const latest = sorted[0];
471
+ if (latest) {
472
+ try {
473
+ const content = readFileSync(latest.path, 'utf-8').trim();
474
+ if (content) {
475
+ return content;
476
+ }
477
+ } catch {
478
+ // Ignore
479
+ }
480
+ }
481
+ }
482
+ } catch {
483
+ // Ignore scan errors
484
+ }
485
+
486
+ // Fallback to simple identity file (for single-agent scenarios)
487
+ const identityPath = join(relayDir, 'mcp-identity');
488
+ if (existsSync(identityPath)) {
489
+ try {
490
+ const content = readFileSync(identityPath, 'utf-8').trim();
491
+ if (content) {
492
+ return content;
493
+ }
494
+ } catch {
495
+ // Ignore read errors
496
+ }
497
+ }
498
+ }
499
+
500
+ // 3. Check outbox directories for a match
501
+ // If only one agent's outbox exists, assume we're that agent
502
+ for (const dir of searchDirs) {
503
+ const outboxDir = join(dir, '.agent-relay', 'outbox');
504
+ if (existsSync(outboxDir)) {
505
+ try {
506
+ const agents = readdirSync(outboxDir, { withFileTypes: true })
507
+ .filter((d) => d.isDirectory())
508
+ .map((d) => d.name);
509
+
510
+ // If there's exactly one outbox, use that agent name
511
+ if (agents.length === 1) {
512
+ return agents[0];
513
+ }
514
+
515
+ // If there are multiple, we can't determine which one we are
516
+ // The wrapper should have created an identity file
517
+ } catch {
518
+ // Ignore read errors
519
+ }
520
+ }
521
+ }
522
+
523
+ return null;
524
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ RelayError,
4
+ DaemonNotRunningError,
5
+ AgentNotFoundError,
6
+ TimeoutError,
7
+ ConnectionError,
8
+ ChannelNotFoundError,
9
+ SpawnError,
10
+ } from './errors.js';
11
+
12
+ describe('Error Classes (single source of truth)', () => {
13
+ describe('RelayError', () => {
14
+ it('creates error with message', () => {
15
+ const err = new RelayError('test error');
16
+ expect(err.message).toBe('test error');
17
+ expect(err.name).toBe('RelayError');
18
+ expect(err).toBeInstanceOf(Error);
19
+ expect(err).toBeInstanceOf(RelayError);
20
+ });
21
+ });
22
+
23
+ describe('DaemonNotRunningError', () => {
24
+ it('creates error with default message', () => {
25
+ const err = new DaemonNotRunningError();
26
+ expect(err.message).toContain('Relay daemon is not running');
27
+ expect(err.name).toBe('DaemonNotRunningError');
28
+ expect(err).toBeInstanceOf(RelayError);
29
+ });
30
+
31
+ it('creates error with custom message', () => {
32
+ const err = new DaemonNotRunningError('Custom msg');
33
+ expect(err.message).toBe('Custom msg');
34
+ });
35
+ });
36
+
37
+ describe('AgentNotFoundError', () => {
38
+ it('includes agent name in message', () => {
39
+ const err = new AgentNotFoundError('MyAgent');
40
+ expect(err.message).toContain('MyAgent');
41
+ expect(err.name).toBe('AgentNotFoundError');
42
+ expect(err).toBeInstanceOf(RelayError);
43
+ });
44
+ });
45
+
46
+ describe('TimeoutError', () => {
47
+ it('includes operation and timeout in message', () => {
48
+ const err = new TimeoutError('spawn', 5000);
49
+ expect(err.message).toContain('5000ms');
50
+ expect(err.message).toContain('spawn');
51
+ expect(err.name).toBe('TimeoutError');
52
+ expect(err).toBeInstanceOf(RelayError);
53
+ });
54
+ });
55
+
56
+ describe('ConnectionError', () => {
57
+ it('includes connection details', () => {
58
+ const err = new ConnectionError('refused');
59
+ expect(err.message).toContain('refused');
60
+ expect(err.name).toBe('ConnectionError');
61
+ expect(err).toBeInstanceOf(RelayError);
62
+ });
63
+ });
64
+
65
+ describe('ChannelNotFoundError', () => {
66
+ it('includes channel name', () => {
67
+ const err = new ChannelNotFoundError('#general');
68
+ expect(err.message).toContain('#general');
69
+ expect(err.name).toBe('ChannelNotFoundError');
70
+ expect(err).toBeInstanceOf(RelayError);
71
+ });
72
+ });
73
+
74
+ describe('SpawnError', () => {
75
+ it('includes worker name and reason', () => {
76
+ const err = new SpawnError('Worker1', 'out of resources');
77
+ expect(err.message).toContain('Worker1');
78
+ expect(err.message).toContain('out of resources');
79
+ expect(err.name).toBe('SpawnError');
80
+ expect(err).toBeInstanceOf(RelayError);
81
+ });
82
+ });
83
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Error Types for Agent Relay
3
+ *
4
+ * Single source of truth for typed error classes.
5
+ * Previously duplicated in @agent-relay/mcp (errors.ts).
6
+ * Now consolidated here in the SDK for shared use.
7
+ */
8
+
9
+ export class RelayError extends Error {
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = 'RelayError';
13
+ }
14
+ }
15
+
16
+ export class DaemonNotRunningError extends RelayError {
17
+ constructor(message?: string) {
18
+ super(message || 'Relay daemon is not running. Start with: agent-relay up');
19
+ this.name = 'DaemonNotRunningError';
20
+ }
21
+ }
22
+
23
+ export class AgentNotFoundError extends RelayError {
24
+ constructor(agentName: string) {
25
+ super(`Agent not found: ${agentName}`);
26
+ this.name = 'AgentNotFoundError';
27
+ }
28
+ }
29
+
30
+ export class TimeoutError extends RelayError {
31
+ constructor(operation: string, timeoutMs: number) {
32
+ super(`Timeout after ${timeoutMs}ms: ${operation}`);
33
+ this.name = 'TimeoutError';
34
+ }
35
+ }
36
+
37
+ export class ConnectionError extends RelayError {
38
+ constructor(message: string) {
39
+ super(`Connection error: ${message}`);
40
+ this.name = 'ConnectionError';
41
+ }
42
+ }
43
+
44
+ export class ChannelNotFoundError extends RelayError {
45
+ constructor(channel: string) {
46
+ super(`Channel not found: ${channel}`);
47
+ this.name = 'ChannelNotFoundError';
48
+ }
49
+ }
50
+
51
+ export class SpawnError extends RelayError {
52
+ constructor(workerName: string, reason: string) {
53
+ super(`Failed to spawn worker "${workerName}": ${reason}`);
54
+ this.name = 'SpawnError';
55
+ }
56
+ }
@@ -110,10 +110,14 @@ export declare class OpenCodeWrapper extends BaseWrapper {
110
110
  */
111
111
  write(data: string): void;
112
112
  /**
113
- * Inject a task into the agent
113
+ * Inject a task into the agent with retry logic.
114
+ *
115
+ * Retries on transient failures to match RelayPtyOrchestrator behavior.
116
+ * This ensures consistent reliability across all wrapper types.
117
+ *
114
118
  * @param task - The task description to inject
115
119
  * @param _from - The sender name (used for formatting)
116
- * @returns true if injection succeeded
120
+ * @returns true if injection succeeded, false otherwise
117
121
  */
118
122
  injectTask(task: string, _from?: string): Promise<boolean>;
119
123
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-wrapper.d.ts","sourceRoot":"","sources":["../src/opencode-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAIxE,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,6BAA6B;IAC7B,OAAO,CAAC,EAAE,iBAAiB,GAAG;QAC5B,kEAAkE;QAClE,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,wEAAwE;QACxE,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,gEAAgE;QAChE,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,qEAAqE;QACrE,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,UAAmB,MAAM,EAAE,qBAAqB,CAAC;IAGjD,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,gBAAgB,CAAS;IAGjC,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAM;IAG1B,OAAO,CAAC,MAAM,CAAe;IAG7B,OAAO,CAAC,YAAY,CAAC,CAAe;gBAExB,MAAM,EAAE,qBAAqB;IAsBnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqCtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3B;;;OAGG;YACW,aAAa;IAa3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;YACW,YAAY;IAsC1B;;OAEG;IACH,OAAO,CAAC,YAAY;IAcpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;cAeX,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE;;OAEG;YACW,oBAAoB;IAOlC;;OAEG;YACW,mBAAmB;IASjC,SAAS,CAAC,cAAc,IAAI,MAAM;IAQlC;;OAEG;YACW,UAAU;IAkBxB;;;OAGG;cACa,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDpD;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,GAAG,SAAS,CAE5B;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzB;;;;;OAKG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUhE;;OAEG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE;IAKnC;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;;;OAIG;IACG,yBAAyB,CAAC,SAAS,SAAQ,EAAE,OAAO,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcnF;;OAEG;IACG,iBAAiB,CAAC,SAAS,SAAQ,EAAE,MAAM,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ1E;;OAEG;IACH,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUxD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAShE"}
1
+ {"version":3,"file":"opencode-wrapper.d.ts","sourceRoot":"","sources":["../src/opencode-wrapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAexE,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,6BAA6B;IAC7B,OAAO,CAAC,EAAE,iBAAiB,GAAG;QAC5B,kEAAkE;QAClE,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,wEAAwE;QACxE,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,gEAAgE;QAChE,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,qEAAqE;QACrE,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,UAAmB,MAAM,EAAE,qBAAqB,CAAC;IAGjD,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,gBAAgB,CAAS;IAGjC,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,YAAY,CAAM;IAG1B,OAAO,CAAC,MAAM,CAAe;IAG7B,OAAO,CAAC,YAAY,CAAC,CAAe;gBAExB,MAAM,EAAE,qBAAqB;IAsBnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqCtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB3B;;;OAGG;YACW,aAAa;IAa3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;YACW,YAAY;IAsC1B;;OAEG;IACH,OAAO,CAAC,YAAY;IAcpB;;OAEG;IACH,OAAO,CAAC,mBAAmB;cAeX,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE;;OAEG;YACW,oBAAoB;IAOlC;;OAEG;YACW,mBAAmB;IASjC,SAAS,CAAC,cAAc,IAAI,MAAM;IAQlC;;OAEG;YACW,UAAU;IAkBxB;;;OAGG;cACa,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDpD;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,GAAG,SAAS,CAE5B;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,GAAG,SAAS,CAEhC;IAED;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMzB;;;;;;;;;OASG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAuBhE;;OAEG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE;IAKnC;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;;;OAIG;IACG,yBAAyB,CAAC,SAAS,SAAQ,EAAE,OAAO,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcnF;;OAEG;IACG,iBAAiB,CAAC,SAAS,SAAQ,EAAE,MAAM,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ1E;;OAEG;IACH,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUxD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAShE"}