@remcp/remcp 0.2.112 → 0.2.114
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/package.json +1 -1
- package/src/agent.mjs +54 -13
package/package.json
CHANGED
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();
|
|
@@ -335,10 +366,23 @@ export async function runAgent(options) {
|
|
|
335
366
|
|
|
336
367
|
function scheduleReconnect(delay) {
|
|
337
368
|
if (stopping || revoked || reconnectTimer) return;
|
|
369
|
+
// Keep the upper-bound guard in control flow immediately before the timer sink. CodeQL's
|
|
370
|
+
// resource-exhaustion model treats this as a barrier, and future callers cannot retain the
|
|
371
|
+
// agent with an attacker-controlled timer lifetime.
|
|
372
|
+
const numericDelay = Number(delay);
|
|
373
|
+
if (!Number.isFinite(numericDelay)) {
|
|
374
|
+
scheduleReconnect(RECONNECT_BASE_MS);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (numericDelay > RECONNECT_MAX_MS) {
|
|
378
|
+
scheduleReconnect(RECONNECT_MAX_MS);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const safeDelay = Math.max(100, Math.floor(numericDelay));
|
|
338
382
|
reconnectTimer = setTimeout(() => {
|
|
339
383
|
reconnectTimer = null;
|
|
340
384
|
connect();
|
|
341
|
-
},
|
|
385
|
+
}, safeDelay);
|
|
342
386
|
reconnectTimer.unref?.();
|
|
343
387
|
}
|
|
344
388
|
|
|
@@ -436,7 +480,7 @@ export async function runAgent(options) {
|
|
|
436
480
|
reconnects,
|
|
437
481
|
});
|
|
438
482
|
console.log(`Connected to ${serverUrl} as ${deviceName}`);
|
|
439
|
-
|
|
483
|
+
sendMetricsSample();
|
|
440
484
|
reportInstallOnce();
|
|
441
485
|
flushTelemetry();
|
|
442
486
|
void checkForUpdate();
|
|
@@ -447,6 +491,7 @@ export async function runAgent(options) {
|
|
|
447
491
|
try { message = JSON.parse(raw.toString()); } catch { return; }
|
|
448
492
|
if (message?.type === 'reconnect') {
|
|
449
493
|
askedToReconnect = true;
|
|
494
|
+
requestedReconnectDelayMs = boundedReconnectDelay(message.delayMs, 150);
|
|
450
495
|
return;
|
|
451
496
|
}
|
|
452
497
|
if (message?.type === 'request') void respond(ws, message);
|
|
@@ -463,10 +508,12 @@ export async function runAgent(options) {
|
|
|
463
508
|
// is not down, it is moving: reconnect at once and forget the backoff, so the person and the
|
|
464
509
|
// model see nothing at all.
|
|
465
510
|
if (code === 1013 || askedToReconnect) {
|
|
511
|
+
const delayMs = requestedReconnectDelayMs || 150;
|
|
466
512
|
askedToReconnect = false;
|
|
513
|
+
requestedReconnectDelayMs = 0;
|
|
467
514
|
reconnects = 0;
|
|
468
|
-
console.log(
|
|
469
|
-
scheduleReconnect(
|
|
515
|
+
console.log(`ReMCP relay is being redeployed; reconnecting in ${delayMs}ms.`);
|
|
516
|
+
scheduleReconnect(delayMs);
|
|
470
517
|
return;
|
|
471
518
|
}
|
|
472
519
|
if (code === 1008 || revoked) {
|
|
@@ -619,13 +666,7 @@ export async function runAgent(options) {
|
|
|
619
666
|
}
|
|
620
667
|
}
|
|
621
668
|
|
|
622
|
-
|
|
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?.();
|
|
669
|
+
scheduleMetricsSample();
|
|
629
670
|
const telemetryFlushTimer = setInterval(flushTelemetry, TELEMETRY_SEND_INTERVAL_MS);
|
|
630
671
|
telemetryFlushTimer.unref?.();
|
|
631
672
|
const updateTimer = setInterval(() => void checkForUpdate(), UPDATE_CHECK_INTERVAL_MS);
|
|
@@ -640,7 +681,7 @@ export async function runAgent(options) {
|
|
|
640
681
|
clearTimeout(reconnectTimer);
|
|
641
682
|
reconnectTimer = null;
|
|
642
683
|
clearPostConnectUpdateChecks();
|
|
643
|
-
if (telemetryTimer)
|
|
684
|
+
if (telemetryTimer) clearTimeout(telemetryTimer);
|
|
644
685
|
clearInterval(telemetryFlushTimer);
|
|
645
686
|
clearInterval(updateTimer);
|
|
646
687
|
for (const controller of inFlight.values()) controller.abort();
|