@mindexec/cli 0.2.140 → 0.2.142

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.140",
3
+ "version": "0.2.142",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -124,6 +124,30 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label }) {
124
124
  };
125
125
  }
126
126
 
127
+ async function startHangingRemoteManager(port) {
128
+ const sockets = new Set();
129
+ const server = net.createServer(socket => {
130
+ sockets.add(socket);
131
+ socket.on('close', () => sockets.delete(socket));
132
+ socket.on('error', () => sockets.delete(socket));
133
+ });
134
+ server.unref();
135
+ await new Promise((resolve, reject) => {
136
+ server.once('error', reject);
137
+ server.listen(port, '127.0.0.1', resolve);
138
+ });
139
+
140
+ return {
141
+ endpoint: `127.0.0.1:${port}`,
142
+ async stop() {
143
+ for (const socket of sockets) {
144
+ socket.destroy();
145
+ }
146
+ await new Promise(resolve => server.close(resolve));
147
+ }
148
+ };
149
+ }
150
+
127
151
  async function waitForBridge(bridge) {
128
152
  return await waitFor(async () => {
129
153
  const result = await fetchJson(`${bridge.baseUrl}/api/status`);
@@ -159,11 +183,13 @@ async function main() {
159
183
  const clientRemotePort = await findFreePort();
160
184
  const stalePort = await findFreePort();
161
185
  const managerEndpoint = `127.0.0.1:${hostRemotePort}`;
186
+ let hangingManager = null;
162
187
  const staleEndpoint = `127.0.0.1:${stalePort}`;
163
188
 
164
189
  let hostBridge = null;
165
190
  let clientBridge = null;
166
191
  try {
192
+ hangingManager = await startHangingRemoteManager(stalePort);
167
193
  hostBridge = spawnBridge({
168
194
  bridgePort: hostBridgePort,
169
195
  remoteHubPort: hostRemotePort,
@@ -180,11 +206,16 @@ async function main() {
180
206
  });
181
207
  await waitForBridge(clientBridge);
182
208
 
209
+ const firstConnectStartedAt = Date.now();
183
210
  const firstAgent = await connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint);
211
+ const firstConnectMs = Date.now() - firstConnectStartedAt;
184
212
  assert.equal(firstAgent.running, true, JSON.stringify(firstAgent));
185
213
  assert.equal(firstAgent.ready, true, JSON.stringify(firstAgent));
186
214
  assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
187
215
  assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
216
+ assert.ok(
217
+ firstConnectMs < 4500,
218
+ `managed RemoteAgent must race slow endpoint candidates instead of waiting sequentially, got ${firstConnectMs}ms`);
188
219
 
189
220
  const connectedDevice = await waitFor(async () => {
190
221
  const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
@@ -218,6 +249,9 @@ async function main() {
218
249
 
219
250
  console.log('RemoteAgent managed supervisor smoke OK');
220
251
  } finally {
252
+ if (hangingManager) {
253
+ await hangingManager.stop();
254
+ }
221
255
  if (clientBridge) {
222
256
  await clientBridge.stop();
223
257
  }
@@ -317,6 +317,20 @@ async function waitForManagedAgent(bridge, managerEndpoint, label, timeoutMs = 2
317
317
  }, timeoutMs, `${label} managed agent\n${bridge.details()}`);
318
318
  }
319
319
 
320
+ async function waitForManagedAgentRestart(bridge, managerEndpoint, previousPid, label, timeoutMs = 20000) {
321
+ return await waitFor(async () => {
322
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
323
+ const agent = result.payload;
324
+ return agent?.running === true
325
+ && agent?.ready === true
326
+ && String(agent.manager || '') === managerEndpoint
327
+ && Number(agent.pid || 0) > 0
328
+ && Number(agent.pid || 0) !== Number(previousPid || 0)
329
+ ? agent
330
+ : null;
331
+ }, timeoutMs, `${label} managed agent restart\n${bridge.details()}`);
332
+ }
333
+
320
334
  async function waitForConnectedDevice(bridge, label) {
321
335
  return await waitFor(async () => {
322
336
  const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
@@ -324,6 +338,18 @@ async function waitForConnectedDevice(bridge, label) {
324
338
  }, 12000, `${label} connected device\n${bridge.details()}`);
325
339
  }
326
340
 
341
+ function killProcess(pid) {
342
+ const value = Number(pid || 0);
343
+ assert.ok(value > 0, `expected positive pid, got ${pid}`);
344
+ try {
345
+ process.kill(value, 'SIGTERM');
346
+ } catch (error) {
347
+ if (error?.code !== 'ESRCH') {
348
+ throw error;
349
+ }
350
+ }
351
+ }
352
+
327
353
  async function main() {
328
354
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
329
355
  let fakeSupabase = null;
@@ -407,6 +433,14 @@ async function main() {
407
433
  assert.equal(secondAgent.leaseId, 'lease-b');
408
434
  await waitForConnectedDevice(hostB, 'host-b');
409
435
 
436
+ const secondPid = Number(secondAgent.pid || 0);
437
+ killProcess(secondPid);
438
+ const restartedAgent = await waitForManagedAgentRestart(client, hostBEndpoint, secondPid, 'host-b', 12000);
439
+ assert.equal(restartedAgent.usingNpx, false, JSON.stringify(restartedAgent));
440
+ assert.match(String(restartedAgent.launcher || ''), /mindexec-remote-fast/i);
441
+ assert.equal(restartedAgent.leaseId, 'lease-b');
442
+ await waitForConnectedDevice(hostB, 'host-b after restart');
443
+
410
444
  currentTarget = {
411
445
  ...currentTarget,
412
446
  active: false,
package/server.js CHANGED
@@ -3132,7 +3132,7 @@ const REMOTE_AGENT_EARLY_EXIT_MS = 1200;
3132
3132
  const REMOTE_AGENT_READY_TIMEOUT_MS = 5000;
3133
3133
  const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
3134
3134
  const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
3135
- const REMOTE_AGENT_RACE_START_STAGGER_MS = 120;
3135
+ const REMOTE_AGENT_RACE_START_STAGGER_MS = Math.max(0, Number(process.env.MINDEXEC_REMOTE_AGENT_RACE_STAGGER_MS ?? 0) || 0);
3136
3136
  const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
3137
3137
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
3138
3138
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
@@ -3151,6 +3151,7 @@ let remoteAgentSyncReportState = null;
3151
3151
  let remoteAgentSyncReportLogKey = '';
3152
3152
  let remoteAgentSyncReportLogAt = 0;
3153
3153
  let remoteAgentConnectPromise = null;
3154
+ const remoteAgentIntentionalStopKeys = new Set();
3154
3155
  const remoteAgentRecentSuccessfulManagers = new Map();
3155
3156
  let remoteAgentRecentSuccessfulManagersLoaded = false;
3156
3157
  let remoteAgentRecentSuccessfulManagersSavePromise = null;
@@ -3656,6 +3657,36 @@ function isRemoteAgentRegistryOwned() {
3656
3657
  && /registry/i.test(String(remoteAgentState.source || ''));
3657
3658
  }
3658
3659
 
3660
+ function isRemoteAgentRegistryState(state) {
3661
+ return /registry/i.test(String(state?.source || ''));
3662
+ }
3663
+
3664
+ function suppressRemoteAgentAutoRetry(connectionKey) {
3665
+ const key = safeRemoteAgentField(connectionKey, 128);
3666
+ if (!key) {
3667
+ return;
3668
+ }
3669
+
3670
+ remoteAgentIntentionalStopKeys.add(key);
3671
+ const timer = setTimeout(() => {
3672
+ remoteAgentIntentionalStopKeys.delete(key);
3673
+ }, 10000);
3674
+ timer?.unref?.();
3675
+ }
3676
+
3677
+ function maybeRetryRemoteAgentAfterExit(state, reason = 'agent-exited') {
3678
+ if (!state
3679
+ || !REMOTE_REGISTRY_FOLLOWER_ENABLED
3680
+ || isShuttingDown
3681
+ || !isRemoteAgentRegistryState(state)
3682
+ || remoteAgentIntentionalStopKeys.has(String(state.connectionKey || ''))) {
3683
+ return;
3684
+ }
3685
+
3686
+ logWarn('remote', `managed RemoteAgent registry process ended; waking registry follower ${formatKeyValue('reason', reason)} ${formatKeyValue('manager', state.manager || '-')}`);
3687
+ scheduleRemoteRegistryFollower(0, reason);
3688
+ }
3689
+
3659
3690
  function isLocalRemoteHostTargetActive() {
3660
3691
  const status = remoteHub.getStatus({ includeSecrets: false });
3661
3692
  return status?.hostTargetActive === true
@@ -3912,6 +3943,7 @@ function resolveNpxCliLauncher() {
3912
3943
 
3913
3944
  async function stopRemoteAgentConnection(reason = 'stopped') {
3914
3945
  const previous = remoteAgentState;
3946
+ suppressRemoteAgentAutoRetry(previous.connectionKey);
3915
3947
  if (previous.proc) {
3916
3948
  try {
3917
3949
  await terminateProcessTree(previous.proc);
@@ -4143,6 +4175,17 @@ function createRemoteAgentAttempt(options = {}) {
4143
4175
  state.lastError = signal ? `signal:${signal}` : `exit:${code}`;
4144
4176
  }
4145
4177
  attempt.error = attempt.error || state.lastError || `exit:${code ?? 'unknown'}`;
4178
+ if (remoteAgentState === state
4179
+ && remoteAgentState.connectionKey === state.connectionKey) {
4180
+ const serialized = serializeRemoteAgentState();
4181
+ if (state.status === 'failed') {
4182
+ logWarn('remote', `managed RemoteAgent failed: ${formatRemoteAgentFailureSummary(state)}`);
4183
+ } else {
4184
+ logEvent('remote', `managed RemoteAgent exited ${formatKeyValue('code', code ?? 0)}`, 'remote');
4185
+ }
4186
+ emitBridgeEvent(state.status === 'exited' ? 'RemoteAgentExited' : 'RemoteAgentFailed', serialized);
4187
+ maybeRetryRemoteAgentAfterExit(state, state.status === 'exited' ? 'agent-exited' : 'agent-failed');
4188
+ }
4146
4189
  });
4147
4190
  } catch (err) {
4148
4191
  attempt.failed = true;