@remcp/remcp 0.2.111 → 0.2.113

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/agent.mjs +40 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.111",
3
+ "version": "0.2.113",
4
4
  "description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/agent.mjs CHANGED
@@ -27,7 +27,7 @@ const POST_CONNECT_UPDATE_RECHECK_DELAYS_MS = Object.freeze([60_000, 5 * 60_000,
27
27
  const POST_CONNECT_UPDATE_RECHECK_JITTER_MS = 30_000;
28
28
  // A version that failed to install is retried after this cooldown instead of on every reconnect.
29
29
  const UPDATE_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
30
- const METRICS_INTERVAL_MS = 60_000;
30
+ const METRICS_INTERVAL_MS = 5 * 60_000;
31
31
  const TELEMETRY_QUEUE_LIMIT = 500;
32
32
  const TELEMETRY_BATCH_LIMIT = 100;
33
33
  const TELEMETRY_SEND_INTERVAL_MS = 5_000;
@@ -71,6 +71,12 @@ function jitter(ms) {
71
71
  return Math.round(ms * (0.75 + Math.random() * 0.5));
72
72
  }
73
73
 
74
+ function boundedReconnectDelay(value, fallbackMs = 150) {
75
+ const parsed = Number(value);
76
+ if (!Number.isFinite(parsed)) return fallbackMs;
77
+ return Math.max(100, Math.min(60_000, Math.floor(parsed)));
78
+ }
79
+
74
80
  // Device health is sampled locally and is the only thing ReMCP stores about the machine
75
81
  // beyond its name, platform and last-seen time. No process list, no file names.
76
82
  function deviceMetrics(extra = {}) {
@@ -98,6 +104,7 @@ export async function runAgent(options) {
98
104
  const agentUrl = serverUrl.replace(/^http/, 'ws') + '/agent';
99
105
  const callTimeoutMs = Math.max(5_000, Number(options.rpcTimeoutMs || 120000) - CALL_TIMEOUT_MARGIN_MS);
100
106
  const telemetryEnabled = options.telemetryEnabled !== false;
107
+ const metricsIntervalMs = Math.max(60_000, Number(options.metricsIntervalMs || process.env.REMCP_AGENT_METRICS_INTERVAL_MS) || METRICS_INTERVAL_MS);
101
108
  const persistState = typeof options.persistState === 'function' ? options.persistState : () => {};
102
109
  const postConnectUpdateRecheckDelaysMs = (Array.isArray(options.updateRecheckDelaysMs)
103
110
  ? options.updateRecheckDelaysMs
@@ -117,6 +124,7 @@ export async function runAgent(options) {
117
124
  let postConnectUpdateTimers = [];
118
125
  // Set when a replica asks this agent to move before it is replaced; the close handler reads it.
119
126
  let askedToReconnect = false;
127
+ let requestedReconnectDelayMs = 0;
120
128
  let pendingRequests = 0;
121
129
  let runtimeVersion = 'unknown';
122
130
  let runtimeRestarts = 0;
@@ -127,6 +135,7 @@ export async function runAgent(options) {
127
135
  let runtimeError = '';
128
136
  const telemetryQueue = [];
129
137
  let telemetryTimer = null;
138
+ let lastMetricsSentAt = 0;
130
139
  // A device whose runtime is missing or unreadable must still run the agent: the agent is what
131
140
  // installs and repairs the runtime, so failing here would remove the only path back.
132
141
  let runtimeEntry = '';
@@ -310,6 +319,28 @@ export async function runAgent(options) {
310
319
  if (!send({ type: 'telemetry', runtimeVersion, agentVersion: VERSION, events: batch })) telemetryQueue.unshift(...batch);
311
320
  }
312
321
 
322
+ function sendMetricsSample({ force = false } = {}) {
323
+ if (activeSocket?.readyState !== 1) return false;
324
+ const at = Date.now();
325
+ // Reconnects during a deploy reuse the same agent process. Do not turn every socket move into
326
+ // another PostgreSQL metrics write; the regular jittered sample is enough.
327
+ if (!force && lastMetricsSentAt && at - lastMetricsSentAt < Math.floor(metricsIntervalMs / 2)) return false;
328
+ const sent = send({ type: 'metrics', runtimeState: runtimeDown ? 'down' : 'ready', runtimeError, metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown, queueDepth: telemetryQueue.length }) });
329
+ if (sent) lastMetricsSentAt = at;
330
+ return sent;
331
+ }
332
+
333
+ function scheduleMetricsSample() {
334
+ if (stopping) return;
335
+ telemetryTimer = setTimeout(() => {
336
+ telemetryTimer = null;
337
+ sendMetricsSample();
338
+ flushTelemetry();
339
+ scheduleMetricsSample();
340
+ }, jitter(metricsIntervalMs));
341
+ telemetryTimer.unref?.();
342
+ }
343
+
313
344
  function queueEvent(event) {
314
345
  if (!telemetryEnabled) return;
315
346
  if (telemetryQueue.length >= TELEMETRY_QUEUE_LIMIT) telemetryQueue.shift();
@@ -436,7 +467,7 @@ export async function runAgent(options) {
436
467
  reconnects,
437
468
  });
438
469
  console.log(`Connected to ${serverUrl} as ${deviceName}`);
439
- send({ type: 'metrics', metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown }), runtimeState: runtimeDown ? 'down' : 'ready', runtimeError });
470
+ sendMetricsSample();
440
471
  reportInstallOnce();
441
472
  flushTelemetry();
442
473
  void checkForUpdate();
@@ -447,6 +478,7 @@ export async function runAgent(options) {
447
478
  try { message = JSON.parse(raw.toString()); } catch { return; }
448
479
  if (message?.type === 'reconnect') {
449
480
  askedToReconnect = true;
481
+ requestedReconnectDelayMs = boundedReconnectDelay(message.delayMs, 150);
450
482
  return;
451
483
  }
452
484
  if (message?.type === 'request') void respond(ws, message);
@@ -463,10 +495,12 @@ export async function runAgent(options) {
463
495
  // is not down, it is moving: reconnect at once and forget the backoff, so the person and the
464
496
  // model see nothing at all.
465
497
  if (code === 1013 || askedToReconnect) {
498
+ const delayMs = requestedReconnectDelayMs || 150;
466
499
  askedToReconnect = false;
500
+ requestedReconnectDelayMs = 0;
467
501
  reconnects = 0;
468
- console.log('ReMCP relay is being redeployed; reconnecting now.');
469
- scheduleReconnect(150);
502
+ console.log(`ReMCP relay is being redeployed; reconnecting in ${delayMs}ms.`);
503
+ scheduleReconnect(delayMs);
470
504
  return;
471
505
  }
472
506
  if (code === 1008 || revoked) {
@@ -619,13 +653,7 @@ export async function runAgent(options) {
619
653
  }
620
654
  }
621
655
 
622
- telemetryTimer = setInterval(() => {
623
- if (activeSocket?.readyState === 1) {
624
- send({ type: 'metrics', runtimeState: runtimeDown ? 'down' : 'ready', runtimeError, metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown, queueDepth: telemetryQueue.length }) });
625
- flushTelemetry();
626
- }
627
- }, METRICS_INTERVAL_MS);
628
- telemetryTimer.unref?.();
656
+ scheduleMetricsSample();
629
657
  const telemetryFlushTimer = setInterval(flushTelemetry, TELEMETRY_SEND_INTERVAL_MS);
630
658
  telemetryFlushTimer.unref?.();
631
659
  const updateTimer = setInterval(() => void checkForUpdate(), UPDATE_CHECK_INTERVAL_MS);
@@ -640,7 +668,7 @@ export async function runAgent(options) {
640
668
  clearTimeout(reconnectTimer);
641
669
  reconnectTimer = null;
642
670
  clearPostConnectUpdateChecks();
643
- if (telemetryTimer) clearInterval(telemetryTimer);
671
+ if (telemetryTimer) clearTimeout(telemetryTimer);
644
672
  clearInterval(telemetryFlushTimer);
645
673
  clearInterval(updateTimer);
646
674
  for (const controller of inFlight.values()) controller.abort();