@mindexec/cli 0.2.122 → 0.2.124

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.122",
3
+ "version": "0.2.124",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -20,12 +20,13 @@
20
20
  "scripts": {
21
21
  "start": "node launch-bridge.cjs",
22
22
  "dev": "node launch-bridge.cjs --watch",
23
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs",
23
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs",
24
24
  "test:auth": "node scripts/auth-session-smoke.mjs",
25
25
  "test:remote": "node scripts/remote-hub-smoke.mjs",
26
26
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
27
27
  "test:remote:render": "node scripts/remote-fleet-render-smoke.mjs",
28
28
  "test:remote:http": "node scripts/remote-http-smoke.mjs",
29
+ "test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
29
30
  "pack:dry": "npm pack --dry-run",
30
31
  "setup:grammars": "node scripts/setup-tree-sitter-grammars.mjs",
31
32
  "postinstall": "npm run setup:grammars"
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { spawn } from 'node:child_process';
5
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
6
+ import net from 'node:net';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const BRIDGE_TOKEN = 'remote-agent-managed-smoke-token';
12
+ const PAIR_TOKEN = 'remote-agent-managed-pair-token';
13
+ const LEASE_ID = 'remote-agent-managed-smoke-lease';
14
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+
16
+ function wait(ms) {
17
+ return new Promise(resolve => setTimeout(resolve, ms));
18
+ }
19
+
20
+ async function findFreePort() {
21
+ return await new Promise((resolve, reject) => {
22
+ const server = net.createServer();
23
+ server.unref();
24
+ server.once('error', reject);
25
+ server.listen(0, '127.0.0.1', () => {
26
+ const address = server.address();
27
+ const port = typeof address === 'object' && address ? address.port : 0;
28
+ server.close(() => resolve(port));
29
+ });
30
+ });
31
+ }
32
+
33
+ async function fetchJson(url, options = {}) {
34
+ const response = await fetch(url, {
35
+ ...options,
36
+ headers: {
37
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
38
+ 'X-Bridge-Token': options.token || BRIDGE_TOKEN,
39
+ ...(options.headers || {})
40
+ }
41
+ });
42
+
43
+ let payload = null;
44
+ try {
45
+ payload = await response.json();
46
+ } catch {
47
+ // Some failure responses may be empty.
48
+ }
49
+
50
+ return {
51
+ status: response.status,
52
+ ok: response.ok,
53
+ payload
54
+ };
55
+ }
56
+
57
+ async function waitFor(predicate, timeoutMs = 30000, label = 'condition') {
58
+ const startedAt = Date.now();
59
+ let lastError = null;
60
+ while (Date.now() - startedAt < timeoutMs) {
61
+ try {
62
+ const value = await predicate();
63
+ if (value) {
64
+ return value;
65
+ }
66
+ } catch (err) {
67
+ lastError = err;
68
+ }
69
+
70
+ await wait(100);
71
+ }
72
+
73
+ throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
74
+ }
75
+
76
+ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label }) {
77
+ const child = spawn(process.execPath, ['server.js'], {
78
+ cwd: LOCAL_BRIDGE_DIR,
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ windowsHide: true,
81
+ env: {
82
+ ...process.env,
83
+ BRIDGE_PORT: String(bridgePort),
84
+ BRIDGE_TOKEN,
85
+ BRIDGE_REQUIRE_TOKEN: '1',
86
+ MINDEXEC_REMOTE_HUB: '1',
87
+ REMOTE_HUB_HOST: '127.0.0.1',
88
+ REMOTE_HUB_PORT: String(remoteHubPort),
89
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
90
+ WORKSPACE_PATH: workspacePath,
91
+ NO_COLOR: '1'
92
+ }
93
+ });
94
+
95
+ let stdout = '';
96
+ let stderr = '';
97
+ child.stdout.on('data', chunk => {
98
+ stdout += chunk.toString();
99
+ });
100
+ child.stderr.on('data', chunk => {
101
+ stderr += chunk.toString();
102
+ });
103
+
104
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
105
+ const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
106
+ const stop = async () => {
107
+ if (child.exitCode === null && !child.killed) {
108
+ child.kill('SIGTERM');
109
+ await Promise.race([
110
+ exitPromise,
111
+ wait(5000)
112
+ ]);
113
+ if (child.exitCode === null && !child.killed) {
114
+ child.kill('SIGKILL');
115
+ }
116
+ }
117
+ };
118
+
119
+ return {
120
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
121
+ child,
122
+ details,
123
+ stop
124
+ };
125
+ }
126
+
127
+ async function waitForBridge(bridge) {
128
+ return await waitFor(async () => {
129
+ const result = await fetchJson(`${bridge.baseUrl}/api/status`);
130
+ return result.ok && result.payload?.status === 'ok' ? result.payload : null;
131
+ }, 30000, `bridge startup\n${bridge.details()}`);
132
+ }
133
+
134
+ async function connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint) {
135
+ const result = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/connect`, {
136
+ method: 'POST',
137
+ body: JSON.stringify({
138
+ managerCandidates: [staleEndpoint, managerEndpoint],
139
+ pairToken: PAIR_TOKEN,
140
+ leaseId: LEASE_ID,
141
+ nodeId: 'remote-agent-managed-smoke-node',
142
+ engine: 'auto',
143
+ source: 'smoke'
144
+ })
145
+ });
146
+
147
+ assert.equal(result.ok, true, JSON.stringify(result.payload));
148
+ assert.equal(result.payload?.ok, true, JSON.stringify(result.payload));
149
+ return result.payload.agent;
150
+ }
151
+
152
+ async function main() {
153
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-managed-smoke-'));
154
+ const hostWorkspace = path.join(tempRoot, 'host');
155
+ const clientWorkspace = path.join(tempRoot, 'client');
156
+ const hostBridgePort = await findFreePort();
157
+ const hostRemotePort = await findFreePort();
158
+ const clientBridgePort = await findFreePort();
159
+ const clientRemotePort = await findFreePort();
160
+ const stalePort = await findFreePort();
161
+ const managerEndpoint = `127.0.0.1:${hostRemotePort}`;
162
+ const staleEndpoint = `127.0.0.1:${stalePort}`;
163
+
164
+ let hostBridge = null;
165
+ let clientBridge = null;
166
+ try {
167
+ hostBridge = spawnBridge({
168
+ bridgePort: hostBridgePort,
169
+ remoteHubPort: hostRemotePort,
170
+ workspacePath: hostWorkspace,
171
+ label: 'host'
172
+ });
173
+ await waitForBridge(hostBridge);
174
+
175
+ clientBridge = spawnBridge({
176
+ bridgePort: clientBridgePort,
177
+ remoteHubPort: clientRemotePort,
178
+ workspacePath: clientWorkspace,
179
+ label: 'client-1'
180
+ });
181
+ await waitForBridge(clientBridge);
182
+
183
+ const firstAgent = await connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint);
184
+ assert.equal(firstAgent.running, true, JSON.stringify(firstAgent));
185
+ assert.equal(firstAgent.ready, true, JSON.stringify(firstAgent));
186
+ assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
187
+ assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
188
+
189
+ const connectedDevice = await waitFor(async () => {
190
+ const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
191
+ return result.payload?.devices?.find(device => device.connected === true) || null;
192
+ }, 10000, `host device registration\n${hostBridge.details()}\n${clientBridge.details()}`);
193
+ assert.equal(connectedDevice.capabilities?.binaryFrames, true);
194
+
195
+ await clientBridge.stop();
196
+ clientBridge = null;
197
+
198
+ const cachePath = path.join(clientWorkspace, '.mindexec', 'cache', 'remote-agent-recent-managers.json');
199
+ const cache = JSON.parse(await readFile(cachePath, 'utf8'));
200
+ assert.equal(cache.version, 1);
201
+ assert.equal(cache.entries?.[0]?.endpoint, managerEndpoint, JSON.stringify(cache));
202
+
203
+ clientBridge = spawnBridge({
204
+ bridgePort: clientBridgePort,
205
+ remoteHubPort: clientRemotePort,
206
+ workspacePath: clientWorkspace,
207
+ label: 'client-2'
208
+ });
209
+ await waitForBridge(clientBridge);
210
+
211
+ const secondAgent = await connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint);
212
+ assert.equal(secondAgent.running, true, JSON.stringify(secondAgent));
213
+ assert.equal(secondAgent.ready, true, JSON.stringify(secondAgent));
214
+ assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
215
+ assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
216
+ assert.equal(secondAgent.managerCandidates?.[0], managerEndpoint, JSON.stringify(secondAgent.managerCandidates));
217
+ assert.equal(secondAgent.managerCandidates?.[1], staleEndpoint, JSON.stringify(secondAgent.managerCandidates));
218
+
219
+ console.log('RemoteAgent managed supervisor smoke OK');
220
+ } finally {
221
+ if (clientBridge) {
222
+ await clientBridge.stop();
223
+ }
224
+ if (hostBridge) {
225
+ await hostBridge.stop();
226
+ }
227
+ await rm(tempRoot, { recursive: true, force: true });
228
+ }
229
+ }
230
+
231
+ await main();
package/server.js CHANGED
@@ -2909,12 +2909,17 @@ const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
2909
2909
  const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
2910
2910
  const REMOTE_AGENT_RACE_START_STAGGER_MS = 120;
2911
2911
  const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
2912
+ const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
2913
+ const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
2914
+ const REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY = 'default';
2912
2915
  let remoteAgentState = createRemoteAgentIdleState();
2913
2916
  let remoteAgentSyncReportState = null;
2914
2917
  let remoteAgentSyncReportLogKey = '';
2915
2918
  let remoteAgentSyncReportLogAt = 0;
2916
2919
  let remoteAgentConnectPromise = null;
2917
2920
  const remoteAgentRecentSuccessfulManagers = new Map();
2921
+ let remoteAgentRecentSuccessfulManagersLoaded = false;
2922
+ let remoteAgentRecentSuccessfulManagersSavePromise = null;
2918
2923
 
2919
2924
  function createRemoteAgentIdleState(overrides = {}) {
2920
2925
  return {
@@ -3216,7 +3221,104 @@ function createRemoteAgentConnectionKey(manager, leaseId) {
3216
3221
  }
3217
3222
 
3218
3223
  function getRemoteAgentRecentManagerKey(leaseId = '') {
3219
- return safeRemoteAgentField(leaseId || 'default', 128) || 'default';
3224
+ return safeRemoteAgentField(leaseId || REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY, 128)
3225
+ || REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY;
3226
+ }
3227
+
3228
+ function getRemoteAgentRecentManagerCachePath() {
3229
+ return path.join(getDataRoot(workspacePath), 'cache', 'remote-agent-recent-managers.json');
3230
+ }
3231
+
3232
+ function normalizeRemoteAgentRecentManagerEntry(entry) {
3233
+ const endpoint = normalizeRemoteManagerEndpoint(entry?.endpoint);
3234
+ const updatedAt = Number(entry?.updatedAt || 0);
3235
+ if (!endpoint || !Number.isFinite(updatedAt) || updatedAt <= 0) {
3236
+ return null;
3237
+ }
3238
+
3239
+ const now = Date.now();
3240
+ if (now - updatedAt > REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS) {
3241
+ return null;
3242
+ }
3243
+
3244
+ return {
3245
+ endpoint,
3246
+ updatedAt
3247
+ };
3248
+ }
3249
+
3250
+ function loadRemoteAgentRecentSuccessfulManagers() {
3251
+ if (remoteAgentRecentSuccessfulManagersLoaded) {
3252
+ return;
3253
+ }
3254
+
3255
+ remoteAgentRecentSuccessfulManagersLoaded = true;
3256
+ const cachePath = getRemoteAgentRecentManagerCachePath();
3257
+ let payload = null;
3258
+ try {
3259
+ payload = JSON.parse(readFileSync(cachePath, 'utf8'));
3260
+ } catch (err) {
3261
+ if (err?.code !== 'ENOENT') {
3262
+ logWarn('remote', `managed RemoteAgent recent manager cache ignored: ${err?.message || err}`);
3263
+ }
3264
+ return;
3265
+ }
3266
+
3267
+ const rawEntries = Array.isArray(payload?.entries)
3268
+ ? payload.entries
3269
+ : Object.entries(payload?.managers || {}).map(([key, value]) => ({ key, ...value }));
3270
+ let loaded = 0;
3271
+ for (const rawEntry of rawEntries) {
3272
+ if (loaded >= REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT) {
3273
+ break;
3274
+ }
3275
+
3276
+ const key = getRemoteAgentRecentManagerKey(rawEntry?.key || rawEntry?.leaseId || '');
3277
+ const entry = normalizeRemoteAgentRecentManagerEntry(rawEntry);
3278
+ if (!entry) {
3279
+ continue;
3280
+ }
3281
+
3282
+ remoteAgentRecentSuccessfulManagers.set(key, entry);
3283
+ loaded += 1;
3284
+ }
3285
+ }
3286
+
3287
+ async function saveRemoteAgentRecentSuccessfulManagers() {
3288
+ const entries = Array.from(remoteAgentRecentSuccessfulManagers.entries())
3289
+ .map(([key, value]) => ({
3290
+ key,
3291
+ endpoint: normalizeRemoteManagerEndpoint(value?.endpoint),
3292
+ updatedAt: Number(value?.updatedAt || 0)
3293
+ }))
3294
+ .map(entry => ({
3295
+ ...entry,
3296
+ normalized: normalizeRemoteAgentRecentManagerEntry(entry)
3297
+ }))
3298
+ .filter(entry => entry.normalized)
3299
+ .sort((left, right) => Number(right.updatedAt || 0) - Number(left.updatedAt || 0))
3300
+ .slice(0, REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT)
3301
+ .map(entry => ({
3302
+ key: entry.key,
3303
+ endpoint: entry.normalized.endpoint,
3304
+ updatedAt: entry.normalized.updatedAt
3305
+ }));
3306
+
3307
+ const payload = {
3308
+ version: 1,
3309
+ savedAt: new Date().toISOString(),
3310
+ entries
3311
+ };
3312
+ await writeFileAtomically(getRemoteAgentRecentManagerCachePath(), JSON.stringify(payload, null, 2), 'utf8');
3313
+ }
3314
+
3315
+ function scheduleRemoteAgentRecentSuccessfulManagersSave() {
3316
+ remoteAgentRecentSuccessfulManagersSavePromise = Promise.resolve(remoteAgentRecentSuccessfulManagersSavePromise)
3317
+ .catch(() => null)
3318
+ .then(() => saveRemoteAgentRecentSuccessfulManagers())
3319
+ .catch(err => {
3320
+ logWarn('remote', `managed RemoteAgent recent manager cache save failed: ${err?.message || err}`);
3321
+ });
3220
3322
  }
3221
3323
 
3222
3324
  function rememberRemoteAgentSuccessfulManager(manager, leaseId = '') {
@@ -3225,10 +3327,14 @@ function rememberRemoteAgentSuccessfulManager(manager, leaseId = '') {
3225
3327
  return;
3226
3328
  }
3227
3329
 
3228
- remoteAgentRecentSuccessfulManagers.set(getRemoteAgentRecentManagerKey(leaseId), {
3330
+ loadRemoteAgentRecentSuccessfulManagers();
3331
+ const entry = {
3229
3332
  endpoint,
3230
3333
  updatedAt: Date.now()
3231
- });
3334
+ };
3335
+ remoteAgentRecentSuccessfulManagers.set(getRemoteAgentRecentManagerKey(leaseId), entry);
3336
+ remoteAgentRecentSuccessfulManagers.set(REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY, entry);
3337
+ scheduleRemoteAgentRecentSuccessfulManagersSave();
3232
3338
  }
3233
3339
 
3234
3340
  function prioritizeRemoteAgentManagers(managers, leaseId = '') {
@@ -3237,8 +3343,10 @@ function prioritizeRemoteAgentManagers(managers, leaseId = '') {
3237
3343
  return normalized;
3238
3344
  }
3239
3345
 
3240
- const recent = remoteAgentRecentSuccessfulManagers.get(getRemoteAgentRecentManagerKey(leaseId));
3241
- const recentEndpoint = normalizeRemoteManagerEndpoint(recent?.endpoint);
3346
+ loadRemoteAgentRecentSuccessfulManagers();
3347
+ const leaseRecent = remoteAgentRecentSuccessfulManagers.get(getRemoteAgentRecentManagerKey(leaseId));
3348
+ const defaultRecent = remoteAgentRecentSuccessfulManagers.get(REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY);
3349
+ const recentEndpoint = normalizeRemoteManagerEndpoint(leaseRecent?.endpoint || defaultRecent?.endpoint);
3242
3350
  if (!recentEndpoint) {
3243
3351
  return normalized;
3244
3352
  }
@@ -10333,6 +10441,12 @@ async function shutdownBridge(signal) {
10333
10441
  // Ignore managed RemoteAgent close errors during shutdown
10334
10442
  }
10335
10443
 
10444
+ try {
10445
+ await remoteAgentRecentSuccessfulManagersSavePromise;
10446
+ } catch {
10447
+ // Ignore recent-manager cache save errors during shutdown
10448
+ }
10449
+
10336
10450
  try {
10337
10451
  await remoteHub.close();
10338
10452
  } catch {