agent-relay 3.1.4 → 3.1.5

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 (42) hide show
  1. package/package.json +8 -8
  2. package/packages/acp-bridge/package.json +2 -2
  3. package/packages/config/package.json +1 -1
  4. package/packages/hooks/package.json +4 -4
  5. package/packages/memory/package.json +2 -2
  6. package/packages/openclaw/dist/config.d.ts +2 -0
  7. package/packages/openclaw/dist/config.d.ts.map +1 -1
  8. package/packages/openclaw/dist/config.js +17 -6
  9. package/packages/openclaw/dist/config.js.map +1 -1
  10. package/packages/openclaw/dist/gateway.d.ts +6 -1
  11. package/packages/openclaw/dist/gateway.d.ts.map +1 -1
  12. package/packages/openclaw/dist/gateway.js +41 -10
  13. package/packages/openclaw/dist/gateway.js.map +1 -1
  14. package/packages/openclaw/dist/inject.d.ts +1 -1
  15. package/packages/openclaw/dist/inject.d.ts.map +1 -1
  16. package/packages/openclaw/dist/inject.js +2 -1
  17. package/packages/openclaw/dist/inject.js.map +1 -1
  18. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  19. package/packages/openclaw/dist/setup.js +81 -10
  20. package/packages/openclaw/dist/setup.js.map +1 -1
  21. package/packages/openclaw/dist/spawn/docker.d.ts.map +1 -1
  22. package/packages/openclaw/dist/spawn/docker.js +2 -1
  23. package/packages/openclaw/dist/spawn/docker.js.map +1 -1
  24. package/packages/openclaw/dist/types.d.ts +2 -0
  25. package/packages/openclaw/dist/types.d.ts.map +1 -1
  26. package/packages/openclaw/dist/types.js +2 -1
  27. package/packages/openclaw/dist/types.js.map +1 -1
  28. package/packages/openclaw/package.json +2 -2
  29. package/packages/openclaw/skill/SKILL.md +61 -84
  30. package/packages/openclaw/src/config.ts +19 -6
  31. package/packages/openclaw/src/gateway.ts +51 -11
  32. package/packages/openclaw/src/inject.ts +2 -2
  33. package/packages/openclaw/src/setup.ts +86 -11
  34. package/packages/openclaw/src/spawn/docker.ts +2 -1
  35. package/packages/openclaw/src/types.ts +3 -0
  36. package/packages/policy/package.json +2 -2
  37. package/packages/sdk/package.json +2 -2
  38. package/packages/sdk-py/pyproject.toml +1 -1
  39. package/packages/telemetry/package.json +1 -1
  40. package/packages/trajectory/package.json +2 -2
  41. package/packages/user-directory/package.json +2 -2
  42. package/packages/utils/package.json +2 -2
@@ -14,7 +14,7 @@ import type {
14
14
  } from '@relaycast/sdk';
15
15
  import WebSocket from 'ws';
16
16
 
17
- import type { GatewayConfig, InboundMessage, DeliveryResult } from './types.js';
17
+ import { DEFAULT_OPENCLAW_GATEWAY_PORT, type GatewayConfig, type InboundMessage, type DeliveryResult } from './types.js';
18
18
  import { SpawnManager } from './spawn/manager.js';
19
19
  import type { SpawnOptions } from './spawn/types.js';
20
20
 
@@ -129,9 +129,14 @@ export class OpenClawGatewayClient {
129
129
  private connectResolve: (() => void) | null = null;
130
130
  private connectReject: ((error: Error) => void) | null = null;
131
131
  private connectTimeout: ReturnType<typeof setTimeout> | null = null;
132
+ private pairingRejected = false;
133
+ private consecutiveFailures = 0;
132
134
 
133
135
  /** Default timeout for initial connection (30 seconds). */
134
136
  private static readonly CONNECT_TIMEOUT_MS = 30_000;
137
+ private static readonly MAX_CONSECUTIVE_FAILURES = 5;
138
+ private static readonly BASE_RECONNECT_MS = 3_000;
139
+ private static readonly MAX_RECONNECT_MS = 30_000;
135
140
 
136
141
  constructor(token: string, port: number) {
137
142
  this.token = token;
@@ -143,6 +148,10 @@ export class OpenClawGatewayClient {
143
148
  async connect(): Promise<void> {
144
149
  if (this.authenticated && this.ws?.readyState === WebSocket.OPEN) return;
145
150
 
151
+ // Explicit connect() clears pairing rejection so users can retry after fixing their token
152
+ this.pairingRejected = false;
153
+ this.stopped = false;
154
+
146
155
  // Cancel any pending reconnect timer to prevent orphaned WebSocket connections
147
156
  if (this.reconnectTimer) {
148
157
  clearTimeout(this.reconnectTimer);
@@ -196,9 +205,18 @@ export class OpenClawGatewayClient {
196
205
  });
197
206
 
198
207
  this.ws.on('close', (code, reason) => {
199
- console.warn(`[openclaw-ws] Disconnected: ${code} ${reason.toString()}`);
208
+ const reasonStr = reason.toString();
209
+ console.warn(`[openclaw-ws] Disconnected: ${code} ${reasonStr}`);
200
210
  const wasAuthenticated = this.authenticated;
201
211
  this.authenticated = false;
212
+
213
+ // Detect pairing rejection via close code 1008 (Policy Violation)
214
+ if (code === 1008 || /pairing|not.paired/i.test(reasonStr)) {
215
+ console.error('[openclaw-ws] Connection closed due to pairing policy. Device is not paired.');
216
+ console.error('[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ~/.openclaw/openclaw.json gateway.auth.token');
217
+ this.pairingRejected = true;
218
+ }
219
+
202
220
  // Reject all pending RPCs
203
221
  for (const [id, pending] of this.pendingRpcs) {
204
222
  clearTimeout(pending.timer);
@@ -213,7 +231,7 @@ export class OpenClawGatewayClient {
213
231
  this.connectReject = null;
214
232
  this.connectResolve = null;
215
233
  }
216
- if (!this.stopped) {
234
+ if (!this.stopped && !this.pairingRejected) {
217
235
  this.scheduleReconnect();
218
236
  }
219
237
  });
@@ -307,14 +325,23 @@ export class OpenClawGatewayClient {
307
325
  if (msg.ok) {
308
326
  console.log('[openclaw-ws] Authenticated successfully');
309
327
  this.authenticated = true;
328
+ this.consecutiveFailures = 0;
310
329
  this.connectResolve?.();
311
330
  this.connectResolve = null;
312
331
  this.connectReject = null;
313
332
  } else {
314
- console.warn(`[openclaw-ws] Auth rejected: ${JSON.stringify(msg.error ?? msg)}`);
315
- // Reject the connect promise on auth failure
316
- const errMsg = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
317
- this.connectReject?.(new Error(`OpenClaw gateway auth failed: ${errMsg}`));
333
+ const errStr = msg.error ? JSON.stringify(msg.error) : 'Authentication rejected';
334
+ const isPairing = /pairing.required|not.paired/i.test(errStr);
335
+
336
+ if (isPairing) {
337
+ console.error('[openclaw-ws] Pairing rejected — device is not paired with the OpenClaw gateway.');
338
+ console.error('[openclaw-ws] Ensure OPENCLAW_GATEWAY_TOKEN matches ~/.openclaw/openclaw.json gateway.auth.token');
339
+ this.pairingRejected = true;
340
+ } else {
341
+ console.warn(`[openclaw-ws] Auth rejected: ${errStr}`);
342
+ }
343
+
344
+ this.connectReject?.(new Error(`OpenClaw gateway auth failed: ${errStr}`));
318
345
  this.connectReject = null;
319
346
  this.connectResolve = null;
320
347
  }
@@ -388,12 +415,25 @@ export class OpenClawGatewayClient {
388
415
  }
389
416
 
390
417
  private scheduleReconnect(): void {
391
- if (this.stopped || this.reconnectTimer) return;
392
- console.log('[openclaw-ws] Reconnecting in 3s...');
418
+ if (this.stopped || this.pairingRejected || this.reconnectTimer) return;
419
+
420
+ this.consecutiveFailures++;
421
+
422
+ if (this.consecutiveFailures >= OpenClawGatewayClient.MAX_CONSECUTIVE_FAILURES) {
423
+ console.warn(`[openclaw-ws] ${this.consecutiveFailures} consecutive connection failures — stopping reconnect.`);
424
+ console.warn('[openclaw-ws] Check that the OpenClaw gateway is running and OPENCLAW_GATEWAY_TOKEN is correct.');
425
+ return;
426
+ }
427
+
428
+ const delay = Math.min(
429
+ OpenClawGatewayClient.BASE_RECONNECT_MS * Math.pow(2, this.consecutiveFailures - 1),
430
+ OpenClawGatewayClient.MAX_RECONNECT_MS,
431
+ );
432
+ console.log(`[openclaw-ws] Reconnecting in ${delay / 1000}s (attempt ${this.consecutiveFailures})...`);
393
433
  this.reconnectTimer = setTimeout(() => {
394
434
  this.reconnectTimer = null;
395
435
  this.doConnect();
396
- }, 3_000);
436
+ }, delay);
397
437
  }
398
438
 
399
439
  async disconnect(): Promise<void> {
@@ -475,7 +515,7 @@ export class InboundGateway {
475
515
 
476
516
  // Connect to the local OpenClaw gateway WebSocket (persistent connection)
477
517
  const token = this.config.openclawGatewayToken ?? process.env.OPENCLAW_GATEWAY_TOKEN;
478
- const port = this.config.openclawGatewayPort ?? 18789;
518
+ const port = this.config.openclawGatewayPort ?? DEFAULT_OPENCLAW_GATEWAY_PORT;
479
519
 
480
520
  if (token) {
481
521
  this.openclawClient = new OpenClawGatewayClient(token, port);
@@ -1,6 +1,6 @@
1
1
  import type { AgentRelayClient, SendMessageInput } from '@agent-relay/sdk';
2
2
 
3
- import type { InboundMessage, DeliveryResult } from './types.js';
3
+ import { DEFAULT_OPENCLAW_GATEWAY_PORT, type InboundMessage, type DeliveryResult } from './types.js';
4
4
 
5
5
  /**
6
6
  * Deliver a message to the local claw using the best available method.
@@ -44,7 +44,7 @@ export async function deliverMessage(
44
44
 
45
45
  // Fallback: OpenClaw OpenResponses API (POST /v1/responses on local gateway)
46
46
  try {
47
- const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? process.env.GATEWAY_PORT ?? '18789';
47
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? process.env.GATEWAY_PORT ?? String(DEFAULT_OPENCLAW_GATEWAY_PORT);
48
48
  const token = process.env.OPENCLAW_GATEWAY_TOKEN;
49
49
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
50
50
  if (token) {
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile, readFile, copyFile } from 'node:fs/promises';
2
+ import { createConnection } from 'node:net';
2
3
  import { join, dirname } from 'node:path';
3
4
  import { existsSync } from 'node:fs';
4
5
  import { hostname } from 'node:os';
@@ -8,7 +9,21 @@ import { spawn as spawnProcess, execFileSync } from 'node:child_process';
8
9
  import { RelayCast } from '@relaycast/sdk';
9
10
 
10
11
  import { detectOpenClaw, saveGatewayConfig } from './config.js';
11
- import type { GatewayConfig } from './types.js';
12
+ import { InboundGateway } from './gateway.js';
13
+ import { DEFAULT_OPENCLAW_GATEWAY_PORT, type GatewayConfig } from './types.js';
14
+
15
+ /**
16
+ * Safely traverse a nested object by dot-separated path.
17
+ * Returns undefined if any segment is missing.
18
+ */
19
+ function extractNestedValue(obj: unknown, path: string): unknown {
20
+ let current: unknown = obj;
21
+ for (const key of path.split('.')) {
22
+ if (current == null || typeof current !== 'object') return undefined;
23
+ current = (current as Record<string, unknown>)[key];
24
+ }
25
+ return current;
26
+ }
12
27
 
13
28
  /**
14
29
  * Resolve how to invoke mcporter. Prefers a global binary, falls back to npx.
@@ -28,6 +43,26 @@ function resolveMcporter(): { cmd: string; prefix: string[] } {
28
43
  }
29
44
  }
30
45
 
46
+ /** Check if a port is already in use by attempting a TCP connection. */
47
+ function isPortInUse(port: number): Promise<boolean> {
48
+ return new Promise((resolve) => {
49
+ const socket = createConnection({ port, host: '127.0.0.1' });
50
+ socket.setTimeout(2000);
51
+ socket.once('connect', () => {
52
+ socket.destroy();
53
+ resolve(true);
54
+ });
55
+ socket.once('timeout', () => {
56
+ socket.destroy();
57
+ resolve(false);
58
+ });
59
+ socket.once('error', () => {
60
+ socket.destroy();
61
+ resolve(false);
62
+ });
63
+ });
64
+ }
65
+
31
66
  export interface SetupOptions {
32
67
  /** If provided, join this workspace. Otherwise create a new one. */
33
68
  apiKey?: string;
@@ -185,12 +220,29 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
185
220
  );
186
221
  }
187
222
 
223
+ // Extract gateway auth from openclaw.json (if available)
224
+ const openclawGatewayToken =
225
+ process.env.OPENCLAW_GATEWAY_TOKEN ??
226
+ (extractNestedValue(detection.config, 'gateway.auth.token') as string | undefined);
227
+ const openclawGatewayPortRaw =
228
+ process.env.OPENCLAW_GATEWAY_PORT ??
229
+ (extractNestedValue(detection.config, 'gateway.port') as number | string | undefined);
230
+ const openclawGatewayPort = openclawGatewayPortRaw ? Number(openclawGatewayPortRaw) : undefined;
231
+
232
+ if (!openclawGatewayToken) {
233
+ console.warn('[setup] No gateway token found in openclaw.json or OPENCLAW_GATEWAY_TOKEN env.');
234
+ console.warn('[setup] Inbound gateway may fail to pair. Set it manually:');
235
+ console.warn('[setup] export OPENCLAW_GATEWAY_TOKEN=$(cat ~/.openclaw/openclaw.json | jq -r .gateway.auth.token)');
236
+ }
237
+
188
238
  // Save gateway config (.env)
189
239
  const gatewayConfig: GatewayConfig = {
190
240
  apiKey,
191
241
  clawName,
192
242
  baseUrl,
193
243
  channels,
244
+ openclawGatewayToken,
245
+ openclawGatewayPort: Number.isFinite(openclawGatewayPort) ? openclawGatewayPort : undefined,
194
246
  };
195
247
  await saveGatewayConfig(gatewayConfig);
196
248
 
@@ -281,18 +333,41 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
281
333
  }
282
334
  }
283
335
 
284
- // Auto-start the inbound gateway in the background
336
+ // Auto-start the inbound gateway in the background, but only if one isn't
337
+ // already running. Re-running setup without this check spawns duplicates
338
+ // that fight over the control port.
285
339
  let gatewayStarted = false;
286
- try {
287
- const child = spawnProcess('npx', ['@agent-relay/openclaw', 'gateway'], {
288
- stdio: 'ignore',
289
- detached: true,
290
- env: { ...process.env, RELAY_API_KEY: apiKey, RELAY_CLAW_NAME: clawName, RELAY_BASE_URL: baseUrl },
291
- });
292
- child.unref();
340
+ // Check the inbound gateway's control port (18790), NOT the OpenClaw
341
+ // gateway WS port (18789) they are different processes.
342
+ const controlPort = Number(process.env.RELAYCAST_CONTROL_PORT) || InboundGateway.DEFAULT_CONTROL_PORT;
343
+ const gatewayAlreadyRunning = await isPortInUse(controlPort);
344
+ if (gatewayAlreadyRunning) {
345
+ console.log('[setup] Inbound gateway already running — skipping spawn.');
293
346
  gatewayStarted = true;
294
- } catch {
295
- // Non-fatal — user can start manually
347
+ } else {
348
+ try {
349
+ const gatewayEnv: Record<string, string> = {
350
+ ...process.env as Record<string, string>,
351
+ RELAY_API_KEY: apiKey,
352
+ RELAY_CLAW_NAME: clawName,
353
+ RELAY_BASE_URL: baseUrl,
354
+ };
355
+ if (openclawGatewayToken) {
356
+ gatewayEnv.OPENCLAW_GATEWAY_TOKEN = openclawGatewayToken;
357
+ }
358
+ if (openclawGatewayPort && Number.isFinite(openclawGatewayPort)) {
359
+ gatewayEnv.OPENCLAW_GATEWAY_PORT = String(openclawGatewayPort);
360
+ }
361
+ const child = spawnProcess('npx', ['@agent-relay/openclaw', 'gateway'], {
362
+ stdio: 'ignore',
363
+ detached: true,
364
+ env: gatewayEnv,
365
+ });
366
+ child.unref();
367
+ gatewayStarted = true;
368
+ } catch {
369
+ // Non-fatal — user can start manually
370
+ }
296
371
  }
297
372
 
298
373
  const parts = [
@@ -8,6 +8,7 @@ import { normalizeModelRef } from '../identity/model.js';
8
8
  import { buildIdentityTask } from '../identity/contract.js';
9
9
  import { buildAgentName } from '../identity/naming.js';
10
10
  import { convertCodexAuth } from '../auth/converter.js';
11
+ import { DEFAULT_OPENCLAW_GATEWAY_PORT } from '../types.js';
11
12
 
12
13
  async function pathExists(targetPath: string): Promise<boolean> {
13
14
  try {
@@ -152,7 +153,7 @@ export class DockerSpawnProvider implements SpawnProvider {
152
153
  const modelRef = normalizeModelRef(options.model, preferredProvider);
153
154
  const workspaceId = options.workspaceId ?? `local-${Date.now().toString(36)}`;
154
155
  const agentName = buildAgentName(workspaceId, options.name);
155
- const gatewayPort = 18789; // Internal to container — each container is isolated
156
+ const gatewayPort = DEFAULT_OPENCLAW_GATEWAY_PORT; // Internal to container — each container is isolated
156
157
  const identityTask = buildIdentityTask(agentName, workspaceId, modelRef);
157
158
  const channels = options.channels?.length ? options.channels : ['general'];
158
159
  const gatewayToken = randomUUID().replace(/-/g, '').slice(0, 32);
@@ -1,3 +1,6 @@
1
+ /** Default port for the local OpenClaw gateway WebSocket API. */
2
+ export const DEFAULT_OPENCLAW_GATEWAY_PORT = 18789;
3
+
1
4
  export interface GatewayConfig {
2
5
  /** Relaycast workspace API key (rk_live_*). */
3
6
  apiKey: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/policy",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
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.1.4"
25
+ "@agent-relay/config": "3.1.5"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -81,7 +81,7 @@
81
81
  "typescript": "^5.7.3"
82
82
  },
83
83
  "dependencies": {
84
- "@agent-relay/config": "3.1.4",
84
+ "@agent-relay/config": "3.1.5",
85
85
  "@relaycast/sdk": "^0.4.0",
86
86
  "yaml": "^2.7.0"
87
87
  }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.1.4"
7
+ version = "3.1.5"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
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.1.4",
3
+ "version": "3.1.5",
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.1.4"
25
+ "@agent-relay/config": "3.1.5"
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.1.4",
3
+ "version": "3.1.5",
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.1.4"
25
+ "@agent-relay/utils": "3.1.5"
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.1.4",
3
+ "version": "3.1.5",
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.1.4",
115
+ "@agent-relay/config": "3.1.5",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {