echoclaw-relay-agent 0.22.2 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -175,7 +175,7 @@ ${BOLD}Usage:${RESET}
175
175
 
176
176
  ${BOLD}Setup Options:${RESET}
177
177
  --relay=URL Relay server URL (default: ${DEFAULT_RELAY})
178
- --bridge-port=PORT Local OpenClaw bridge port (default: 8013)
178
+ --bridge-port=PORT Local OpenClaw bridge port (default: 18789)
179
179
  -h, --help Show this help
180
180
 
181
181
  ${BOLD}How it works:${RESET}
@@ -5,9 +5,15 @@
5
5
  * Requires N consecutive failures before declaring bridge down (failureThreshold).
6
6
  * Uses jittered intervals to prevent synchronized spikes.
7
7
  *
8
- * V2: When bridge goes down, attempts to restart the OpenClaw process
8
+ * When bridge goes down, attempts to restart the OpenClaw process
9
9
  * via platform service manager (launchctl on macOS, systemctl on Linux).
10
10
  * Rate limited: max 3 restart attempts per 10 minute window.
11
+ *
12
+ * Fixes applied (external AI review):
13
+ * - Restart retries on every tick while down (not just on state transition)
14
+ * - 5s cooldown after restart before next health check
15
+ * - Immediate re-check after successful restart
16
+ * - Exceptions in _check() count as failures (not silently swallowed)
11
17
  */
12
18
  import { EventEmitter } from 'node:events';
13
19
  import { TokenDiscovery } from './TokenDiscovery.js';
@@ -37,13 +43,16 @@ export declare class GatewayWatchdog extends EventEmitter {
37
43
  start(): void;
38
44
  /** Stop health checks. Idempotent / re-entrant safe. */
39
45
  stop(): void;
40
- /** Run one check, then schedule next with fresh jitter. Prevents overlap. */
46
+ /** Schedule the next tick with jitter. */
47
+ private _scheduleNext;
48
+ /** Run one check, then schedule next. Prevents overlap. */
41
49
  private _tick;
42
50
  private _check;
43
51
  private _transition;
44
52
  /**
45
- * Attempt to restart the OpenClaw process when bridge goes down.
53
+ * Attempt to restart the OpenClaw process when bridge is down.
46
54
  * Rate limited: max 3 attempts per 10 minute window.
55
+ * Called on every tick while down — internal guards prevent over-restarting.
47
56
  */
48
57
  private _attemptRestart;
49
58
  }
@@ -5,9 +5,15 @@
5
5
  * Requires N consecutive failures before declaring bridge down (failureThreshold).
6
6
  * Uses jittered intervals to prevent synchronized spikes.
7
7
  *
8
- * V2: When bridge goes down, attempts to restart the OpenClaw process
8
+ * When bridge goes down, attempts to restart the OpenClaw process
9
9
  * via platform service manager (launchctl on macOS, systemctl on Linux).
10
10
  * Rate limited: max 3 restart attempts per 10 minute window.
11
+ *
12
+ * Fixes applied (external AI review):
13
+ * - Restart retries on every tick while down (not just on state transition)
14
+ * - 5s cooldown after restart before next health check
15
+ * - Immediate re-check after successful restart
16
+ * - Exceptions in _check() count as failures (not silently swallowed)
11
17
  */
12
18
  import { EventEmitter } from 'node:events';
13
19
  import { restartOpenClaw } from './ProcessRestart.js';
@@ -15,6 +21,7 @@ import { DEFAULT_GATEWAY_CONFIG } from './types.js';
15
21
  // ── Restart rate limiting ────────────────────────────────────
16
22
  const RESTART_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
17
23
  const MAX_RESTARTS_PER_WINDOW = 3;
24
+ const RESTART_COOLDOWN_MS = 5000; // wait for process to start before next check
18
25
  export class GatewayWatchdog extends EventEmitter {
19
26
  constructor(config) {
20
27
  super();
@@ -72,7 +79,7 @@ export class GatewayWatchdog extends EventEmitter {
72
79
  writable: true,
73
80
  value: false
74
81
  });
75
- // V2: Restart tracking
82
+ // Restart tracking
76
83
  Object.defineProperty(this, "_restartTimestamps", {
77
84
  enumerable: true,
78
85
  configurable: true,
@@ -121,17 +128,28 @@ export class GatewayWatchdog extends EventEmitter {
121
128
  }
122
129
  }
123
130
  // ── Private ────────────────────────────────────────────────
124
- /** Run one check, then schedule next with fresh jitter. Prevents overlap. */
131
+ /** Schedule the next tick with jitter. */
132
+ _scheduleNext(delayMs) {
133
+ if (this._stopped)
134
+ return;
135
+ const jitter = 1 + (Math.random() * 0.4 - 0.2);
136
+ const delay = delayMs ?? Math.round(this.checkIntervalMs * jitter);
137
+ this.timer = setTimeout(() => this._tick(), delay);
138
+ }
139
+ /** Run one check, then schedule next. Prevents overlap. */
125
140
  _tick() {
126
141
  this._check()
127
- .catch(() => { })
142
+ .catch((err) => {
143
+ // Unexpected error in check logic itself — count as failure
144
+ this.consecutiveFailures++;
145
+ if (this.consecutiveFailures >= this.failureThreshold) {
146
+ this._transition('down');
147
+ this._attemptRestart();
148
+ }
149
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
150
+ })
128
151
  .finally(() => {
129
- if (this._stopped)
130
- return;
131
- // Per-tick jitter (±20%) — recalculated each cycle
132
- const jitter = 1 + (Math.random() * 0.4 - 0.2);
133
- const delayMs = Math.round(this.checkIntervalMs * jitter);
134
- this.timer = setTimeout(() => this._tick(), delayMs);
152
+ this._scheduleNext();
135
153
  });
136
154
  }
137
155
  async _check() {
@@ -151,7 +169,7 @@ export class GatewayWatchdog extends EventEmitter {
151
169
  }
152
170
  if (alive) {
153
171
  this.consecutiveFailures = 0;
154
- // V2: Reset restart exhaustion on recovery
172
+ // Reset restart exhaustion on recovery
155
173
  if (this._status === 'down') {
156
174
  this._restartExhausted = false;
157
175
  }
@@ -161,6 +179,9 @@ export class GatewayWatchdog extends EventEmitter {
161
179
  this.consecutiveFailures++;
162
180
  if (this.consecutiveFailures >= this.failureThreshold) {
163
181
  this._transition('down');
182
+ // Attempt restart on every tick while down (not just on state transition).
183
+ // _attemptRestart has its own rate limiting.
184
+ this._attemptRestart();
164
185
  }
165
186
  // Below threshold: stay in current state (don't flap on transient failures)
166
187
  }
@@ -185,14 +206,13 @@ export class GatewayWatchdog extends EventEmitter {
185
206
  baseUrl: this._endpoint?.baseUrl,
186
207
  consecutiveFailures: this.consecutiveFailures,
187
208
  });
188
- // V2: Attempt restart
189
- this._attemptRestart();
190
209
  }
191
210
  }
192
- // ── V2: Process restart ────────────────────────────────────
211
+ // ── Process restart ────────────────────────────────────
193
212
  /**
194
- * Attempt to restart the OpenClaw process when bridge goes down.
213
+ * Attempt to restart the OpenClaw process when bridge is down.
195
214
  * Rate limited: max 3 attempts per 10 minute window.
215
+ * Called on every tick while down — internal guards prevent over-restarting.
196
216
  */
197
217
  _attemptRestart() {
198
218
  if (this._restartInProgress || this._restartExhausted || this._stopped)
@@ -222,6 +242,18 @@ export class GatewayWatchdog extends EventEmitter {
222
242
  method: result.method,
223
243
  output: result.output,
224
244
  });
245
+ // After successful restart, wait for process to start then re-check
246
+ if (!this._stopped) {
247
+ // Cancel the already-scheduled next tick, replace with cooldown + immediate check
248
+ if (this.timer) {
249
+ clearTimeout(this.timer);
250
+ this.timer = null;
251
+ }
252
+ this.timer = setTimeout(() => {
253
+ if (!this._stopped)
254
+ this._tick();
255
+ }, RESTART_COOLDOWN_MS);
256
+ }
225
257
  }
226
258
  else {
227
259
  this.emit('restart_failed', {
@@ -10,11 +10,11 @@ import type { TunnelPayload } from 'echoclaw-crypto';
10
10
  export interface GatewayConfig {
11
11
  /** Enable the gateway layer. When false, RelayAgent behaves as V1. */
12
12
  enabled: boolean;
13
- /** Local bridge port. Default: 8013 */
13
+ /** Local bridge port. Default: 18789 */
14
14
  bridgePort?: number;
15
15
  /** Local bridge host. Default: 'localhost'. Forced to loopback only. */
16
16
  bridgeHost?: string;
17
- /** Ports to probe during discovery. Default: [8013, 8080] */
17
+ /** Ports to probe during discovery. Default: [18789] */
18
18
  discoveryPorts?: number[];
19
19
  /** Health check interval in ms. Default: 30_000 */
20
20
  healthCheckIntervalMs?: number;
@@ -36,9 +36,9 @@ export interface GatewayConfig {
36
36
  deviceName?: string;
37
37
  }
38
38
  export declare const DEFAULT_GATEWAY_CONFIG: {
39
- readonly bridgePort: 8013;
39
+ readonly bridgePort: 18789;
40
40
  readonly bridgeHost: "localhost";
41
- readonly discoveryPorts: readonly [8013, 8080];
41
+ readonly discoveryPorts: readonly [18789];
42
42
  readonly healthCheckIntervalMs: 30000;
43
43
  readonly healthCheckPath: "/health";
44
44
  readonly heartbeatIntervalMs: 10000;
@@ -7,9 +7,9 @@
7
7
  * Key design: Ack + Heartbeat + Push async model for long-running AI tasks.
8
8
  */
9
9
  export const DEFAULT_GATEWAY_CONFIG = {
10
- bridgePort: 8013,
10
+ bridgePort: 18789,
11
11
  bridgeHost: 'localhost',
12
- discoveryPorts: [8013, 8080],
12
+ discoveryPorts: [18789],
13
13
  healthCheckIntervalMs: 30000,
14
14
  healthCheckPath: '/health',
15
15
  heartbeatIntervalMs: 10000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoclaw-relay-agent",
3
- "version": "0.22.2",
3
+ "version": "0.22.3",
4
4
  "description": "EchoClaw Relay Connection — E2E encrypted relay transport, pairing, and session management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",