@remcp/remcp 0.2.31 → 0.2.32

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 +24 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
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
@@ -25,6 +25,9 @@ const TELEMETRY_QUEUE_LIMIT = 500;
25
25
  const TELEMETRY_BATCH_LIMIT = 100;
26
26
  const TELEMETRY_SEND_INTERVAL_MS = 5_000;
27
27
  const RECONNECT_BASE_MS = 2_000;
28
+ // The first retry after an unexpected drop: fast enough that a blip is invisible, slow enough not to
29
+ // hammer a server that is genuinely down.
30
+ const RECONNECT_FIRST_MS = 500;
28
31
  // Must stay above the runtime's own output ceiling (8 MiB), otherwise a large but legal tool result
29
32
  // closes the stdio connection and restarts the runtime mid-call.
30
33
  const RUNTIME_STDIO_BUFFER_BYTES = 24 * 1024 * 1024;
@@ -240,6 +243,8 @@ export async function runAgent(options) {
240
243
  const inFlight = new Map();
241
244
  let activeSocket;
242
245
  let reconnects = 0;
246
+ // Set when a replica asks this agent to move before it is replaced; the close handler reads it.
247
+ let askedToReconnect = false;
243
248
  let pendingRequests = 0;
244
249
  let runtimeVersion = 'unknown';
245
250
  let runtimeRestarts = 0;
@@ -444,6 +449,10 @@ export async function runAgent(options) {
444
449
  ws.on('message', raw => {
445
450
  let message;
446
451
  try { message = JSON.parse(raw.toString()); } catch { return; }
452
+ if (message?.type === 'reconnect') {
453
+ askedToReconnect = true;
454
+ return;
455
+ }
447
456
  if (message?.type === 'request') void respond(ws, message);
448
457
  if (message?.type === 'cancel' && message.id) {
449
458
  const controller = inFlight.get(message.id);
@@ -453,6 +462,16 @@ export async function runAgent(options) {
453
462
  ws.on('close', code => {
454
463
  for (const controller of inFlight.values()) controller.abort();
455
464
  if (stopping) return;
465
+ // 1013 ('reconnect') is what a replica sends before it is replaced by a deployment. The machine
466
+ // is not down, it is moving: reconnect at once and forget the backoff, so the person and the
467
+ // model see nothing at all.
468
+ if (code === 1013 || askedToReconnect) {
469
+ askedToReconnect = false;
470
+ reconnects = 0;
471
+ console.log('ReMCP relay is being redeployed; reconnecting now.');
472
+ setTimeout(connect, 150);
473
+ return;
474
+ }
456
475
  if (code === 1008 || revoked) {
457
476
  // The relay closes with 1008 when the device was revoked. Retrying forever would
458
477
  // hide that from the person at the computer.
@@ -469,7 +488,11 @@ export async function runAgent(options) {
469
488
  return;
470
489
  }
471
490
  reconnects += 1;
472
- const delay = jitter(Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.min(reconnects, 5)));
491
+ // The first retry after an unexpected drop is quick on purpose: a deploy blip, a proxy restart or
492
+ // a dropped packet should cost a fraction of a second, not the two seconds the backoff starts at.
493
+ const delay = reconnects === 1
494
+ ? jitter(RECONNECT_FIRST_MS)
495
+ : jitter(Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.min(reconnects, 5)));
473
496
  setTimeout(connect, delay);
474
497
  });
475
498
  ws.on('error', error => console.error(`ReMCP relay: ${error.message}`));