@mindexec/cli 0.2.160 → 0.2.162

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.160",
3
+ "version": "0.2.162",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -291,6 +291,8 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label
291
291
  MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
292
292
  MINDEXEC_REMOTE_REGISTRY_POLL_MS: '10000',
293
293
  MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
294
+ MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT: '2',
295
+ MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS: '500',
294
296
  MINDEXEC_REMOTE_REGISTRY_REALTIME_RECONNECT_MS: '250',
295
297
  MINDEXEC_REMOTE_REGISTRY_REALTIME_DEBOUNCE_MS: '100',
296
298
  MINDEXEC_REMOTE_HOST_TARGET_RENEW_MS: '5000',
@@ -575,6 +577,28 @@ async function main() {
575
577
  assert.equal(secondAgent.leaseId, 'lease-b');
576
578
  await waitForConnectedDevice(hostB, 'host-b');
577
579
 
580
+ await wait(250);
581
+ currentTarget = null;
582
+ fakeSupabase.broadcastChange('DELETE');
583
+ await waitFor(async () => {
584
+ const status = await fetchJson(`${client.baseUrl}/api/status`);
585
+ const agent = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
586
+ return status.payload?.remoteRegistryFollower?.reason === 'no-active-target-agent-kept'
587
+ && agent.payload?.running === true
588
+ && String(agent.payload?.manager || '') === hostBEndpoint
589
+ ? { status: status.payload.remoteRegistryFollower, agent: agent.payload }
590
+ : null;
591
+ }, 4000, `transient no-active-target keeps managed agent\n${client.details()}`);
592
+
593
+ currentTarget = createRegistryTarget({
594
+ endpoint: hostBEndpoint,
595
+ endpointCandidates: [hostBEndpoint],
596
+ leaseId: 'lease-b'
597
+ });
598
+ fakeSupabase.broadcastChange('INSERT');
599
+ const restoredAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b after transient missing target', 9000);
600
+ assert.equal(Number(restoredAgent.pid || 0), Number(secondAgent.pid || 0), JSON.stringify(restoredAgent));
601
+
578
602
  const secondPid = Number(secondAgent.pid || 0);
579
603
  killProcess(secondPid);
580
604
  const restartedAgent = await waitForManagedAgentRestart(client, hostBEndpoint, secondPid, 'host-b', 12000);
@@ -589,6 +613,8 @@ async function main() {
589
613
  expires_at: new Date(Date.now() - 1000).toISOString()
590
614
  };
591
615
  fakeSupabase.broadcastChange('UPDATE');
616
+ await wait(700);
617
+ fakeSupabase.broadcastChange('UPDATE');
592
618
 
593
619
  await waitFor(async () => {
594
620
  const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
package/server.js CHANGED
@@ -3360,6 +3360,8 @@ const REMOTE_REGISTRY_FOLLOWER_ENABLED = !/^(0|false|no|off)$/i.test(String(proc
3360
3360
  const REMOTE_REGISTRY_FOLLOWER_POLL_MS = Math.max(1500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_POLL_MS || 5000) || 5000);
3361
3361
  const REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS || 1200) || 1200);
3362
3362
  const REMOTE_REGISTRY_FOLLOWER_MISSING_SESSION_STOP_COUNT = 3;
3363
+ const REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT = Math.max(2, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_STOP_COUNT || 8) || 8);
3364
+ const REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_REGISTRY_INACTIVE_GRACE_MS || 60000) || 60000);
3363
3365
  const REMOTE_REGISTRY_REALTIME_ENABLED = REMOTE_REGISTRY_FOLLOWER_ENABLED
3364
3366
  && !/^(0|false|no|off)$/i.test(String(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME || 'true'));
3365
3367
  const REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS = Math.max(10000, Number(process.env.MINDEXEC_REMOTE_REGISTRY_REALTIME_HEARTBEAT_MS || 25000) || 25000);
@@ -3391,6 +3393,8 @@ let remoteRegistryFollowerTimer = null;
3391
3393
  let remoteRegistryFollowerInFlight = false;
3392
3394
  let remoteRegistryFollowerStarted = false;
3393
3395
  let remoteRegistryFollowerConsecutiveMissingSession = 0;
3396
+ let remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3397
+ let remoteRegistryFollowerInactiveTargetFirstAt = 0;
3394
3398
  let remoteRegistryRealtimeSocket = null;
3395
3399
  let remoteRegistryRealtimeSessionKey = '';
3396
3400
  let remoteRegistryRealtimeTopic = '';
@@ -3532,6 +3536,9 @@ function createRemoteAgentSyncReport(body = {}) {
3532
3536
  targetLeaseId: safeRemoteAgentField(body.targetLeaseId || body.TargetLeaseId, 128),
3533
3537
  targetNodeId: safeRemoteAgentField(body.targetNodeId || body.TargetNodeId, 128),
3534
3538
  targetExpiresAt: safeRemoteAgentField(body.targetExpiresAt || body.TargetExpiresAt, 80),
3539
+ inactiveCount: Number(body.inactiveCount ?? body.InactiveCount ?? 0) || 0,
3540
+ inactiveElapsedMs: Number(body.inactiveElapsedMs ?? body.InactiveElapsedMs ?? 0) || 0,
3541
+ agentKept: body.agentKept === true || body.AgentKept === true,
3535
3542
  updatedAt: new Date().toISOString()
3536
3543
  };
3537
3544
  }
@@ -3954,6 +3961,39 @@ function maybeRetryRemoteAgentAfterExit(state, reason = 'agent-exited') {
3954
3961
  scheduleRemoteRegistryFollower(0, reason);
3955
3962
  }
3956
3963
 
3964
+ function resetRemoteRegistryInactiveTargetGrace() {
3965
+ remoteRegistryFollowerConsecutiveInactiveTarget = 0;
3966
+ remoteRegistryFollowerInactiveTargetFirstAt = 0;
3967
+ }
3968
+
3969
+ function rememberRemoteRegistryInactiveTarget(reason = 'no-active-target') {
3970
+ const now = Date.now();
3971
+ remoteRegistryFollowerConsecutiveInactiveTarget += 1;
3972
+ if (!remoteRegistryFollowerInactiveTargetFirstAt) {
3973
+ remoteRegistryFollowerInactiveTargetFirstAt = now;
3974
+ }
3975
+
3976
+ const elapsedMs = Math.max(0, now - remoteRegistryFollowerInactiveTargetFirstAt);
3977
+ const agentRunning = isRemoteAgentRegistryOwned();
3978
+ const confirmedInactive = remoteRegistryFollowerConsecutiveInactiveTarget >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT
3979
+ && elapsedMs >= REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS;
3980
+
3981
+ if (agentRunning && !confirmedInactive) {
3982
+ logEvent(
3983
+ 'remote',
3984
+ `registry target temporarily inactive; keeping managed RemoteAgent ${formatKeyValue('reason', reason)} ${formatKeyValue('count', remoteRegistryFollowerConsecutiveInactiveTarget)} ${formatKeyValue('elapsedMs', elapsedMs)}`,
3985
+ 'remote');
3986
+ }
3987
+
3988
+ return {
3989
+ count: remoteRegistryFollowerConsecutiveInactiveTarget,
3990
+ elapsedMs,
3991
+ agentRunning,
3992
+ confirmedInactive,
3993
+ agentKept: agentRunning && !confirmedInactive
3994
+ };
3995
+ }
3996
+
3957
3997
  function isLocalRemoteHostTargetActive() {
3958
3998
  const status = remoteHub.getStatus({ includeSecrets: false });
3959
3999
  return status?.hostTargetActive === true
@@ -4208,9 +4248,11 @@ function resolveNpxCliLauncher() {
4208
4248
  return '';
4209
4249
  }
4210
4250
 
4211
- async function stopRemoteAgentConnection(reason = 'stopped') {
4251
+ async function stopRemoteAgentConnection(reason = 'stopped', options = {}) {
4212
4252
  const previous = remoteAgentState;
4213
- suppressRemoteAgentAutoRetry(previous.connectionKey);
4253
+ if (options.suppressRetry !== false) {
4254
+ suppressRemoteAgentAutoRetry(previous.connectionKey);
4255
+ }
4214
4256
  if (previous.proc) {
4215
4257
  try {
4216
4258
  await terminateProcessTree(previous.proc);
@@ -4753,7 +4795,13 @@ function serializeRemoteRegistryFollowerState() {
4753
4795
  return {
4754
4796
  ...remoteRegistryFollowerState,
4755
4797
  pollMs: REMOTE_REGISTRY_FOLLOWER_POLL_MS,
4756
- fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS
4798
+ fastRetryMs: REMOTE_REGISTRY_FOLLOWER_FAST_RETRY_MS,
4799
+ inactiveStopCount: REMOTE_REGISTRY_FOLLOWER_INACTIVE_STOP_COUNT,
4800
+ inactiveGraceMs: REMOTE_REGISTRY_FOLLOWER_INACTIVE_GRACE_MS,
4801
+ inactiveCount: remoteRegistryFollowerConsecutiveInactiveTarget,
4802
+ inactiveElapsedMs: remoteRegistryFollowerInactiveTargetFirstAt
4803
+ ? Math.max(0, Date.now() - remoteRegistryFollowerInactiveTargetFirstAt)
4804
+ : 0
4757
4805
  };
4758
4806
  }
4759
4807
 
@@ -5499,7 +5547,11 @@ function buildRemoteHostTargetRegistryPayload(hub) {
5499
5547
  const hostInstanceId = safeRemoteAgentField(hub?.hostTargetHostInstanceId || hub?.hostInstanceId, 128);
5500
5548
  const nodeId = safeRemoteAgentField(hub?.hostTargetNodeId, 128);
5501
5549
  const activatedAt = safeRemoteAgentField(hub?.hostTargetActivatedAt, 80) || new Date().toISOString();
5502
- const expiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80) || new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5550
+ const rawExpiresAt = safeRemoteAgentField(hub?.hostTargetExpiresAt, 80);
5551
+ const rawExpiresAtMs = Date.parse(rawExpiresAt || '');
5552
+ const expiresAt = Number.isFinite(rawExpiresAtMs) && rawExpiresAtMs > Date.now() + 5000
5553
+ ? rawExpiresAt
5554
+ : new Date(Date.now() + REMOTE_HOST_TARGET_LEASE_MS).toISOString();
5503
5555
 
5504
5556
  if (!pairToken || !leaseId || !hostInstanceId || !nodeId) {
5505
5557
  return {
@@ -5710,7 +5762,7 @@ async function reconcileRemoteAfterSystemResume(trigger = 'timer-drift', driftMs
5710
5762
  closeRemoteRegistryRealtime('system-resume');
5711
5763
 
5712
5764
  if (wasRegistryAgentRunning) {
5713
- await stopRemoteAgentConnection('system-resume-reconnect');
5765
+ await stopRemoteAgentConnection('system-resume-reconnect', { suppressRetry: false });
5714
5766
  }
5715
5767
 
5716
5768
  if (isLocalHost) {
@@ -6001,6 +6053,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6001
6053
  const target = await fetchRemoteRegistryTarget(config, session);
6002
6054
 
6003
6055
  if (localHub?.hostTargetActive === true) {
6056
+ resetRemoteRegistryInactiveTargetGrace();
6004
6057
  if (target?.active === true
6005
6058
  && !isRemoteRegistryTargetExpired(target)
6006
6059
  && !isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
@@ -6064,32 +6117,50 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
6064
6117
  }
6065
6118
 
6066
6119
  if (!target?.active || isRemoteRegistryTargetExpired(target)) {
6067
- if (isRemoteAgentRegistryOwned()) {
6120
+ const inactiveReason = target
6121
+ ? 'registry-inactive'
6122
+ : 'no-active-target';
6123
+ const inactive = rememberRemoteRegistryInactiveTarget(inactiveReason);
6124
+ if (inactive.confirmedInactive && isRemoteAgentRegistryOwned()) {
6068
6125
  await stopRemoteAgentConnection('registry-inactive');
6069
6126
  }
6127
+ const keptManager = inactive.agentKept ? safeRemoteAgentField(remoteAgentState.manager, 160) : '';
6128
+ const keptCandidates = inactive.agentKept ? normalizeRemoteManagerEndpointList(remoteAgentState.managerCandidates, remoteAgentState.manager) : [];
6070
6129
  updateRemoteRegistryFollowerState({
6071
- status: 'idle',
6072
- reason: 'no-active-target',
6130
+ status: inactive.agentKept ? 'waiting' : 'idle',
6131
+ reason: inactive.agentKept ? `${inactiveReason}-agent-kept` : inactiveReason,
6073
6132
  authenticated: true,
6074
6133
  lastAttemptAt: attemptedAt,
6075
6134
  lastSuccessAt: attemptedAt,
6076
- targetEndpoint: '',
6077
- targetEndpointCandidates: [],
6078
- targetLeaseId: '',
6079
- targetNodeId: '',
6135
+ targetEndpoint: keptManager,
6136
+ targetEndpointCandidates: keptCandidates,
6137
+ targetLeaseId: inactive.agentKept ? safeRemoteAgentField(remoteAgentState.leaseId, 128) : '',
6138
+ targetNodeId: inactive.agentKept ? safeRemoteAgentField(remoteAgentState.nodeId, 128) : '',
6139
+ inactiveCount: inactive.count,
6140
+ inactiveElapsedMs: inactive.elapsedMs,
6141
+ agentKept: inactive.agentKept,
6080
6142
  lastError: ''
6081
6143
  });
6082
6144
  await reportRemoteRegistryFollowerSync({
6083
6145
  ok: true,
6084
6146
  skipped: true,
6085
- reason: 'no-active-target',
6147
+ reason: inactive.agentKept ? `${inactiveReason}-agent-kept` : inactiveReason,
6086
6148
  trigger,
6087
- authenticated: true
6149
+ authenticated: true,
6150
+ targetEndpoint: keptManager,
6151
+ targetEndpointCandidates: keptCandidates,
6152
+ targetLeaseId: inactive.agentKept ? remoteAgentState.leaseId : '',
6153
+ targetNodeId: inactive.agentKept ? remoteAgentState.nodeId : '',
6154
+ inactiveCount: inactive.count,
6155
+ inactiveElapsedMs: inactive.elapsedMs,
6156
+ agentKept: inactive.agentKept
6088
6157
  });
6089
6158
  scheduleRemoteRegistryFollower(REMOTE_REGISTRY_FOLLOWER_POLL_MS, 'no-target');
6090
6159
  return serializeRemoteRegistryFollowerState();
6091
6160
  }
6092
6161
 
6162
+ resetRemoteRegistryInactiveTargetGrace();
6163
+
6093
6164
  if (isRemoteRegistryTargetSameAsLocalHost(localHub, target)) {
6094
6165
  updateRemoteRegistryFollowerState({
6095
6166
  status: 'skipped',