@remcp/remcp 0.2.197 → 0.2.199
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-relay-connection.mjs +20 -22
- package/src/agent-update-monitor.mjs +14 -3
- package/src/cli/service.mjs +21 -2
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@ import { nextDeployReconnect } from './agent-deploy-reconnect.mjs';
|
|
|
9
9
|
const RECONNECT_BASE_MS = 2_000;
|
|
10
10
|
const RECONNECT_FIRST_MS = 500;
|
|
11
11
|
const RECONNECT_MAX_MS = 60_000;
|
|
12
|
-
|
|
13
12
|
// Higher-level agent owns runtime supervision/telemetry; this controller owns only relay transport.
|
|
14
13
|
export function createAgentRelayConnection({
|
|
15
14
|
agentUrl, serverUrl, deviceToken, deviceId, deviceName,
|
|
@@ -22,6 +21,7 @@ export function createAgentRelayConnection({
|
|
|
22
21
|
let reconnectTimer = null;
|
|
23
22
|
let askedToReconnect = false;
|
|
24
23
|
let requestedReconnectDelayMs = 0;
|
|
24
|
+
let requestedReconnectReason = '';
|
|
25
25
|
let connectedAt = 0;
|
|
26
26
|
let rapidDeployDisconnects = 0;
|
|
27
27
|
function scheduleReconnect(delay) {
|
|
@@ -121,6 +121,8 @@ export function createAgentRelayConnection({
|
|
|
121
121
|
if (message?.type === 'reconnect') {
|
|
122
122
|
askedToReconnect = true;
|
|
123
123
|
requestedReconnectDelayMs = boundedReconnectDelay(message.delayMs, 150);
|
|
124
|
+
requestedReconnectReason = ['deploy', 'credentials', 'capacity'].includes(message.reason)
|
|
125
|
+
? message.reason : 'transient';
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
126
128
|
if (message?.type === 'request') void respond(ws, message);
|
|
@@ -133,37 +135,33 @@ export function createAgentRelayConnection({
|
|
|
133
135
|
clearPostConnectUpdateChecks();
|
|
134
136
|
for (const controller of inFlight.values()) controller.abort();
|
|
135
137
|
if (isStopping()) return;
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
// A replacement owner or revoked device takes precedence over a reconnect hint.
|
|
139
|
+
if (code === 1008 || revoked) {
|
|
140
|
+
console.error('ReMCP access for this device was revoked. Pair the machine again from the ReMCP workspace: remcp connect --server <url> --code <code> --install');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (code === 1012) {
|
|
144
|
+
console.error('Another ReMCP agent connected for this device; this process will stop. Run one agent per machine (a service manager or the remcp start command).');
|
|
145
|
+
markStopping();
|
|
146
|
+
void stop().finally(() => setTimeout(() => process.exit(0), 100));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
139
149
|
if (code === 1013 || askedToReconnect) {
|
|
150
|
+
// 1013 covers transient overload and relay-store failures, not just deployment.
|
|
151
|
+
// Preserve a fast first handover while bounding repeated short-lived sessions.
|
|
140
152
|
const reconnect = nextDeployReconnect({
|
|
141
153
|
requestedDelayMs: requestedReconnectDelayMs || 150,
|
|
142
154
|
rapidDisconnects: rapidDeployDisconnects,
|
|
143
155
|
connectionUptimeMs: connectedAt ? Date.now() - connectedAt : 0,
|
|
144
156
|
});
|
|
145
157
|
rapidDeployDisconnects = reconnect.rapidDisconnects;
|
|
146
|
-
const
|
|
158
|
+
const reason = requestedReconnectReason || 'transient';
|
|
147
159
|
askedToReconnect = false;
|
|
160
|
+
requestedReconnectReason = '';
|
|
148
161
|
requestedReconnectDelayMs = 0;
|
|
149
162
|
setReconnects(0);
|
|
150
|
-
console.log(`ReMCP relay
|
|
151
|
-
scheduleReconnect(delayMs);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (code === 1008 || revoked) {
|
|
155
|
-
// The relay closes with 1008 when the device was revoked. Retrying forever would
|
|
156
|
-
// hide that from the person at the computer.
|
|
157
|
-
console.error('ReMCP access for this device was revoked. Pair the machine again from the ReMCP workspace: remcp connect --server <url> --code <code> --install');
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (code === 1012) {
|
|
161
|
-
// 1012 ('service restart') is what the relay sends when another agent process took over this
|
|
162
|
-
// device. Staying alive would keep a second runtime and a reconnect loop, so this process
|
|
163
|
-
// stops and leaves the device to the agent that owns the connection.
|
|
164
|
-
console.error('Another ReMCP agent connected for this device; this process will stop. Run one agent per machine (a service manager or the remcp start command).');
|
|
165
|
-
markStopping();
|
|
166
|
-
void stop().finally(() => setTimeout(() => process.exit(0), 100));
|
|
163
|
+
console.log(`ReMCP relay reconnect (${reason}); retrying in ${reconnect.delayMs}ms.`);
|
|
164
|
+
scheduleReconnect(reconnect.delayMs);
|
|
167
165
|
return;
|
|
168
166
|
}
|
|
169
167
|
scheduleBackoffReconnect();
|
|
@@ -6,7 +6,9 @@ import { fetchUpdateManifest } from './fetch-update-manifest.mjs';
|
|
|
6
6
|
import { globalCliEntry, globalInstalledVersion, isNewer, restartToApplyUpdate, updateDecision, updateInvocationArgs } from './agent-update.mjs';
|
|
7
7
|
|
|
8
8
|
export const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
9
|
-
const UPDATE_CHECK_TIMEOUT_MS =
|
|
9
|
+
const UPDATE_CHECK_TIMEOUT_MS = 12000;
|
|
10
|
+
// A flapping relay must not trigger update discovery on every successful reconnect.
|
|
11
|
+
const UPDATE_CHECK_RECONNECT_GAP_MS = 30000;
|
|
10
12
|
const POST_CONNECT_UPDATE_RECHECK_DELAYS_MS = Object.freeze([60_000, 5 * 60_000, 15 * 60_000]);
|
|
11
13
|
const POST_CONNECT_UPDATE_RECHECK_JITTER_MS = 30_000;
|
|
12
14
|
const UPDATE_RETRY_COOLDOWN_MS = 30 * 60 * 1000;
|
|
@@ -32,6 +34,7 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
|
|
|
32
34
|
let lastAttemptAt = 0;
|
|
33
35
|
let lastUpdateCheckError = '';
|
|
34
36
|
let lastUpdateCheckErrorAt = 0;
|
|
37
|
+
let lastDiscoveryAt = 0;
|
|
35
38
|
|
|
36
39
|
function clearPostConnectUpdateChecks() {
|
|
37
40
|
for (const timer of postConnectUpdateTimers) clearTimeout(timer);
|
|
@@ -47,16 +50,24 @@ export function createAgentUpdateMonitor({ options, serverUrl, queueEvent, getRu
|
|
|
47
50
|
: 0;
|
|
48
51
|
const timer = setTimeout(() => {
|
|
49
52
|
postConnectUpdateTimers = postConnectUpdateTimers.filter(candidate => candidate !== timer);
|
|
50
|
-
void checkForUpdate();
|
|
53
|
+
void checkForUpdate({ scheduled: true });
|
|
51
54
|
}, delay + jitter);
|
|
52
55
|
timer.unref?.();
|
|
53
56
|
postConnectUpdateTimers.push(timer);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
async function checkForUpdate() {
|
|
60
|
+
async function checkForUpdate({ scheduled = false } = {}) {
|
|
58
61
|
if (options.autoUpdate === false) return;
|
|
59
62
|
if (updateInFlight) return;
|
|
63
|
+
const minimumGap = Number.isFinite(Number(options.updateCheckMinGapMs))
|
|
64
|
+
? Math.max(0, Math.min(UPDATE_CHECK_RECONNECT_GAP_MS, Number(options.updateCheckMinGapMs)))
|
|
65
|
+
: UPDATE_CHECK_RECONNECT_GAP_MS;
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
// Scheduled bounded convergence polls are distinct from reconnect-triggered discovery.
|
|
68
|
+
// Do not starve release-pause/Redis-recovery checks behind the reconnect storm cooldown.
|
|
69
|
+
if (!scheduled && lastDiscoveryAt && now - lastDiscoveryAt < minimumGap) return;
|
|
70
|
+
lastDiscoveryAt = now;
|
|
60
71
|
try {
|
|
61
72
|
// This request contains no local file payload; the config-derived URL is the explicitly paired relay.
|
|
62
73
|
// codeql[js/file-access-to-http]
|
package/src/cli/service.mjs
CHANGED
|
@@ -23,9 +23,28 @@ export function quoteSystemd(value) {
|
|
|
23
23
|
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// A systemd user's PATH may resolve system npm even when the CLI lives under nvm.
|
|
27
|
+
// Pin the npm installation target to the CLI's own existing global prefix.
|
|
28
|
+
export function linuxServiceLauncherText(cliPath, nodePath) {
|
|
29
|
+
if (!path.isAbsolute(cliPath) || !path.isAbsolute(nodePath) || path.basename(path.dirname(cliPath)) !== 'bin') {
|
|
30
|
+
throw new Error('Linux service needs absolute CLI and Node paths in a global bin directory');
|
|
31
|
+
}
|
|
32
|
+
const prefix = path.dirname(path.dirname(cliPath));
|
|
27
33
|
const cliScript = resolvedCliScript(cliPath);
|
|
28
|
-
const
|
|
34
|
+
const lines = [
|
|
35
|
+
'#!/bin/sh',
|
|
36
|
+
'NPM_CONFIG_PREFIX=' + shellQuote(prefix),
|
|
37
|
+
'export NPM_CONFIG_PREFIX',
|
|
38
|
+
'PATH=' + shellQuote(path.dirname(nodePath)) + ':"$PATH"',
|
|
39
|
+
'export PATH',
|
|
40
|
+
'exec ' + shellQuote(nodePath) + ' ' + shellQuote(cliScript) + ' "$@"',
|
|
41
|
+
'',
|
|
42
|
+
];
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeLinuxServiceLauncher(cliPath = globalCliPath(), nodePath = process.execPath) {
|
|
47
|
+
const launcher = linuxServiceLauncherText(cliPath, nodePath);
|
|
29
48
|
fs.mkdirSync(path.dirname(linuxServiceLauncherFile), { recursive: true, mode: 0o700 });
|
|
30
49
|
const temporary = `${linuxServiceLauncherFile}.${process.pid}.${Date.now()}.tmp`;
|
|
31
50
|
fs.writeFileSync(temporary, launcher, { mode: 0o700 });
|