@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.
- package/package.json +1 -1
- package/src/agent.mjs +40 -12
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();
|
|
@@ -436,7 +467,7 @@ export async function runAgent(options) {
|
|
|
436
467
|
reconnects,
|
|
437
468
|
});
|
|
438
469
|
console.log(`Connected to ${serverUrl} as ${deviceName}`);
|
|
439
|
-
|
|
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(
|
|
469
|
-
scheduleReconnect(
|
|
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
|
-
|
|
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)
|
|
671
|
+
if (telemetryTimer) clearTimeout(telemetryTimer);
|
|
644
672
|
clearInterval(telemetryFlushTimer);
|
|
645
673
|
clearInterval(updateTimer);
|
|
646
674
|
for (const controller of inFlight.values()) controller.abort();
|