@remcp/remcp 0.2.42 → 0.2.43
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 +44 -13
package/package.json
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -77,6 +77,7 @@ export async function runAgent(options) {
|
|
|
77
77
|
const inFlight = new Map();
|
|
78
78
|
let activeSocket;
|
|
79
79
|
let reconnects = 0;
|
|
80
|
+
let reconnectTimer = null;
|
|
80
81
|
// Set when a replica asks this agent to move before it is replaced; the close handler reads it.
|
|
81
82
|
let askedToReconnect = false;
|
|
82
83
|
let pendingRequests = 0;
|
|
@@ -219,6 +220,26 @@ export async function runAgent(options) {
|
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
function scheduleReconnect(delay) {
|
|
224
|
+
if (stopping || revoked || reconnectTimer) return;
|
|
225
|
+
reconnectTimer = setTimeout(() => {
|
|
226
|
+
reconnectTimer = null;
|
|
227
|
+
connect();
|
|
228
|
+
}, delay);
|
|
229
|
+
reconnectTimer.unref?.();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function scheduleBackoffReconnect() {
|
|
233
|
+
if (stopping || revoked || reconnectTimer) return;
|
|
234
|
+
reconnects += 1;
|
|
235
|
+
// The first retry after an unexpected drop is quick on purpose: a deploy blip, a proxy restart or
|
|
236
|
+
// a dropped packet should cost a fraction of a second, not the two seconds the backoff starts at.
|
|
237
|
+
const delay = reconnects === 1
|
|
238
|
+
? jitter(RECONNECT_FIRST_MS)
|
|
239
|
+
: jitter(Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.min(reconnects, 5)));
|
|
240
|
+
scheduleReconnect(delay);
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
async function respond(ws, message) {
|
|
223
244
|
pendingRequests += 1;
|
|
224
245
|
// The relay forwards a cancel when the MCP client goes away. Without it a cancelled tool call
|
|
@@ -250,15 +271,29 @@ export async function runAgent(options) {
|
|
|
250
271
|
function connect() {
|
|
251
272
|
if (stopping) return;
|
|
252
273
|
const ws = new WebSocket(agentUrl, { headers: { Authorization: `Bearer ${deviceToken}` } });
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
// The ws client leaves cleanup/retry to the caller when an unexpected-response listener exists.
|
|
275
|
+
// A temporary workspace pause therefore needs an explicit retry; otherwise the first 423 leaves
|
|
276
|
+
// this WebSocket stuck in CONNECTING forever and turning the device back on cannot recover it.
|
|
277
|
+
ws.on('unexpected-response', (request, response) => {
|
|
278
|
+
const handshakeRevoked = String(response.headers['x-remcp-revoked'] || '') === '1';
|
|
279
|
+
const handshakeDisabled = String(response.headers['x-remcp-disabled'] || '') === '1';
|
|
280
|
+
if (handshakeRevoked) revoked = true;
|
|
281
|
+
console.error(`ReMCP relay refused the connection (HTTP ${response.statusCode})${handshakeRevoked ? ': this device was revoked' : handshakeDisabled ? ': this device is temporarily disabled' : ''}.`);
|
|
258
282
|
response.resume();
|
|
283
|
+
request.destroy();
|
|
284
|
+
if (handshakeDisabled && !revoked) {
|
|
285
|
+
reconnects += 1;
|
|
286
|
+
// Service access is a user-controlled pause, not an outage. Poll slowly enough not to hammer
|
|
287
|
+
// the relay, but cap recovery so an enable action becomes effective within seconds.
|
|
288
|
+
scheduleReconnect(jitter(RECONNECT_BASE_MS));
|
|
289
|
+
}
|
|
259
290
|
});
|
|
260
291
|
activeSocket = ws;
|
|
261
292
|
ws.on('open', () => {
|
|
293
|
+
if (reconnectTimer) {
|
|
294
|
+
clearTimeout(reconnectTimer);
|
|
295
|
+
reconnectTimer = null;
|
|
296
|
+
}
|
|
262
297
|
reconnects = 0;
|
|
263
298
|
ws.send(JSON.stringify({
|
|
264
299
|
type: 'hello',
|
|
@@ -303,7 +338,7 @@ export async function runAgent(options) {
|
|
|
303
338
|
askedToReconnect = false;
|
|
304
339
|
reconnects = 0;
|
|
305
340
|
console.log('ReMCP relay is being redeployed; reconnecting now.');
|
|
306
|
-
|
|
341
|
+
scheduleReconnect(150);
|
|
307
342
|
return;
|
|
308
343
|
}
|
|
309
344
|
if (code === 1008 || revoked) {
|
|
@@ -321,13 +356,7 @@ export async function runAgent(options) {
|
|
|
321
356
|
void stop().finally(() => setTimeout(() => process.exit(0), 100));
|
|
322
357
|
return;
|
|
323
358
|
}
|
|
324
|
-
|
|
325
|
-
// The first retry after an unexpected drop is quick on purpose: a deploy blip, a proxy restart or
|
|
326
|
-
// a dropped packet should cost a fraction of a second, not the two seconds the backoff starts at.
|
|
327
|
-
const delay = reconnects === 1
|
|
328
|
-
? jitter(RECONNECT_FIRST_MS)
|
|
329
|
-
: jitter(Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** Math.min(reconnects, 5)));
|
|
330
|
-
setTimeout(connect, delay);
|
|
359
|
+
scheduleBackoffReconnect();
|
|
331
360
|
});
|
|
332
361
|
ws.on('error', error => console.error(`ReMCP relay: ${error.message}`));
|
|
333
362
|
}
|
|
@@ -443,6 +472,8 @@ export async function runAgent(options) {
|
|
|
443
472
|
if (stopPromise) return stopPromise;
|
|
444
473
|
stopping = true;
|
|
445
474
|
clearTimeout(runtimeRestartTimer);
|
|
475
|
+
clearTimeout(reconnectTimer);
|
|
476
|
+
reconnectTimer = null;
|
|
446
477
|
if (telemetryTimer) clearInterval(telemetryTimer);
|
|
447
478
|
clearInterval(telemetryFlushTimer);
|
|
448
479
|
clearInterval(updateTimer);
|