@remcp/remcp 0.2.56 → 0.2.58

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 +39 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
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
@@ -21,6 +21,10 @@ export { localRuntimeEntry, supervisorRestart, updateDecision } from './agent-up
21
21
 
22
22
  const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
23
23
  const UPDATE_CHECK_TIMEOUT_MS = 5000;
24
+ // A deployment can briefly leave one HA replica advertising the previous release. Recheck a few
25
+ // times after a stable connection instead of making that unlucky first answer stick for six hours.
26
+ const POST_CONNECT_UPDATE_RECHECK_DELAYS_MS = Object.freeze([60_000, 5 * 60_000, 15 * 60_000]);
27
+ const POST_CONNECT_UPDATE_RECHECK_JITTER_MS = 30_000;
24
28
  // A version that failed to install is retried after this cooldown instead of on every reconnect.
25
29
  const UPDATE_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
26
30
  const METRICS_INTERVAL_MS = 60_000;
@@ -75,12 +79,22 @@ export async function runAgent(options) {
75
79
  const callTimeoutMs = Math.max(5_000, Number(options.rpcTimeoutMs || 120000) - CALL_TIMEOUT_MARGIN_MS);
76
80
  const telemetryEnabled = options.telemetryEnabled !== false;
77
81
  const persistState = typeof options.persistState === 'function' ? options.persistState : () => {};
82
+ const postConnectUpdateRecheckDelaysMs = (Array.isArray(options.updateRecheckDelaysMs)
83
+ ? options.updateRecheckDelaysMs
84
+ : POST_CONNECT_UPDATE_RECHECK_DELAYS_MS)
85
+ .map(Number)
86
+ .filter(delay => Number.isFinite(delay) && delay >= 0 && delay < UPDATE_CHECK_INTERVAL_MS)
87
+ .slice(0, 3);
88
+ const postConnectUpdateRecheckJitterMs = Number.isFinite(Number(options.updateRecheckJitterMs))
89
+ ? Math.max(0, Math.min(60_000, Number(options.updateRecheckJitterMs)))
90
+ : POST_CONNECT_UPDATE_RECHECK_JITTER_MS;
78
91
  let stopping = false;
79
92
  let revoked = false;
80
93
  const inFlight = new Map();
81
94
  let activeSocket;
82
95
  let reconnects = 0;
83
96
  let reconnectTimer = null;
97
+ let postConnectUpdateTimers = [];
84
98
  // Set when a replica asks this agent to move before it is replaced; the close handler reads it.
85
99
  let askedToReconnect = false;
86
100
  let pendingRequests = 0;
@@ -377,6 +391,7 @@ export async function runAgent(options) {
377
391
  reportInstallOnce();
378
392
  flushTelemetry();
379
393
  void checkForUpdate();
394
+ schedulePostConnectUpdateChecks();
380
395
  });
381
396
  ws.on('message', raw => {
382
397
  let message;
@@ -392,6 +407,7 @@ export async function runAgent(options) {
392
407
  }
393
408
  });
394
409
  ws.on('close', code => {
410
+ clearPostConnectUpdateChecks();
395
411
  for (const controller of inFlight.values()) controller.abort();
396
412
  if (stopping) return;
397
413
  // 1013 ('reconnect') is what a replica sends before it is replaced by a deployment. The machine
@@ -436,6 +452,28 @@ export async function runAgent(options) {
436
452
  let lastAttemptAt = 0;
437
453
  let lastUpdateCheckError = '';
438
454
  let lastUpdateCheckErrorAt = 0;
455
+
456
+ function clearPostConnectUpdateChecks() {
457
+ for (const timer of postConnectUpdateTimers) clearTimeout(timer);
458
+ postConnectUpdateTimers = [];
459
+ }
460
+
461
+ function schedulePostConnectUpdateChecks() {
462
+ clearPostConnectUpdateChecks();
463
+ if (options.autoUpdate === false) return;
464
+ for (const delay of postConnectUpdateRecheckDelaysMs) {
465
+ const jitter = postConnectUpdateRecheckJitterMs > 0
466
+ ? Math.floor(Math.random() * (postConnectUpdateRecheckJitterMs + 1))
467
+ : 0;
468
+ const timer = setTimeout(() => {
469
+ postConnectUpdateTimers = postConnectUpdateTimers.filter(candidate => candidate !== timer);
470
+ void checkForUpdate();
471
+ }, delay + jitter);
472
+ timer.unref?.();
473
+ postConnectUpdateTimers.push(timer);
474
+ }
475
+ }
476
+
439
477
  async function checkForUpdate() {
440
478
  if (options.autoUpdate === false) return;
441
479
  if (updateInFlight) return;
@@ -550,6 +588,7 @@ export async function runAgent(options) {
550
588
  runtimeToolsRetryTimer = null;
551
589
  clearTimeout(reconnectTimer);
552
590
  reconnectTimer = null;
591
+ clearPostConnectUpdateChecks();
553
592
  if (telemetryTimer) clearInterval(telemetryTimer);
554
593
  clearInterval(telemetryFlushTimer);
555
594
  clearInterval(updateTimer);