@mindexec/cli 0.2.141 → 0.2.143

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.141",
3
+ "version": "0.2.143",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -442,6 +442,7 @@ async function loadCss3DManager() {
442
442
  this.readyState = MiniWebSocket.CONNECTING;
443
443
  this.binaryType = '';
444
444
  this.sent = [];
445
+ this.closeCount = 0;
445
446
  webSocketConnections.push(this);
446
447
  setTimeout(() => {
447
448
  if (this.readyState !== MiniWebSocket.CONNECTING) return;
@@ -458,6 +459,7 @@ async function loadCss3DManager() {
458
459
 
459
460
  close() {
460
461
  if (this.readyState === MiniWebSocket.CLOSED) return;
462
+ this.closeCount += 1;
461
463
  this.readyState = MiniWebSocket.CLOSED;
462
464
  this.onclose?.({ type: 'close' });
463
465
  }
@@ -490,6 +492,8 @@ async function loadCss3DManager() {
490
492
  Set,
491
493
  WeakMap,
492
494
  Blob,
495
+ TextDecoder,
496
+ TextEncoder,
493
497
  requestAnimationFrame: callback => setTimeout(() => callback(Date.now()), 0),
494
498
  cancelAnimationFrame: id => clearTimeout(id),
495
499
  createImageBitmap: async blob => {
@@ -546,6 +550,18 @@ function wait(ms = 0) {
546
550
  return new Promise(resolve => setTimeout(resolve, ms));
547
551
  }
548
552
 
553
+ function encodeRemoteBinaryFrame(metadata, payload = new Uint8Array([0xff, 0xd8, 0xff, 0xd9])) {
554
+ const metaBytes = new TextEncoder().encode(JSON.stringify(metadata));
555
+ const payloadBytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
556
+ const buffer = new ArrayBuffer(4 + metaBytes.byteLength + payloadBytes.byteLength);
557
+ const view = new DataView(buffer);
558
+ view.setUint32(0, metaBytes.byteLength);
559
+ const bytes = new Uint8Array(buffer);
560
+ bytes.set(metaBytes, 4);
561
+ bytes.set(payloadBytes, 4 + metaBytes.byteLength);
562
+ return buffer;
563
+ }
564
+
549
565
  function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTaskBatches = [], lastError = '') {
550
566
  const connected = devices.filter(device => device.Connected).length;
551
567
  const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
@@ -935,6 +951,52 @@ try {
935
951
  binaryDrawCalls.some(call => call.op === 'clearRect' || call.op === 'fillRect'),
936
952
  false,
937
953
  'binary Blob frame paint must not clear/fill the canvas before drawing');
954
+
955
+ await wait(20);
956
+ const liveWs = diagnostics.webSocketConnections[0];
957
+ assert.ok(liveWs, 'expected MDM render to open the binary frame WebSocket');
958
+ assert.equal(liveWs.readyState, 1, 'binary frame WebSocket should be open before rerender');
959
+ const wsCountBeforeRerender = diagnostics.webSocketConnections.length;
960
+ const wsCloseCountBeforeRerender = liveWs.closeCount;
961
+ const websocketFrameSeq = 10001;
962
+ liveWs.onmessage?.({
963
+ data: new Blob([encodeRemoteBinaryFrame({
964
+ type: 'remote.frame.binary',
965
+ deviceId: patchTarget.DeviceId,
966
+ kind: 'live',
967
+ frameSeq: websocketFrameSeq,
968
+ streamId: 'render-smoke-ws-live',
969
+ contentHash: `render-smoke-ws-live-${websocketFrameSeq}`,
970
+ mimeType: 'image/jpeg',
971
+ width: 64,
972
+ height: 36,
973
+ capturedAt: new Date().toISOString(),
974
+ receivedAt: new Date().toISOString()
975
+ })])
976
+ });
977
+ await wait(30);
978
+ assert.equal(
979
+ patchPreview.dataset.remoteFleetFrameSeq,
980
+ String(websocketFrameSeq),
981
+ JSON.stringify(diagnostics.runtimeTrace.slice(-12)));
982
+
983
+ manager.renderRemoteFleetMonitorForTest(bodyView, buildMonitorNode(devices, hub.getStatus({ includeSecrets: true }), latestTaskBatch, recentTaskBatches));
984
+ await wait(30);
985
+ assert.equal(
986
+ liveWs.closeCount,
987
+ wsCloseCountBeforeRerender,
988
+ 'MDM rerender must not close the active binary frame WebSocket');
989
+ assert.equal(
990
+ diagnostics.webSocketConnections.length,
991
+ wsCountBeforeRerender,
992
+ 'MDM rerender must reuse the active binary frame WebSocket instead of opening a replacement');
993
+ const rerenderedPatchPreview = bodyView
994
+ .querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`)
995
+ ?.querySelector('[data-remote-fleet-device-preview="tile"]');
996
+ assert.ok(rerenderedPatchPreview);
997
+ assert.equal(rerenderedPatchPreview.dataset.remoteFleetFrameSeq, String(websocketFrameSeq));
998
+ assert.equal(rerenderedPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display, 'block');
999
+
938
1000
  const alternateDevice = devices.find(device =>
939
1001
  device.Connected === true
940
1002
  && device.DeviceId !== focusedDevice.DeviceId);
@@ -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
@@ -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;
@@ -13459,13 +13459,15 @@
13459
13459
  const REMOTE_FLEET_FRAME_CANVAS_MAX_DPR = 2;
13460
13460
  const REMOTE_FLEET_FRAME_BLOB_CACHE_MS = 10000;
13461
13461
  const REMOTE_FLEET_FRAME_BLOB_CACHE_LIMIT = 96;
13462
- const REMOTE_FLEET_LIVE_FRAME_DECODE_TIMEOUT_MS = 180;
13462
+ const REMOTE_FLEET_LIVE_FRAME_DECODE_TIMEOUT_MS = 700;
13463
13463
  const REMOTE_FLEET_THUMBNAIL_FRAME_DECODE_TIMEOUT_MS = 1200;
13464
13464
  const REMOTE_FLEET_BINARY_FRAME_MAX_DECODE_IN_FLIGHT = 1;
13465
13465
  const REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS = 1500;
13466
13466
  const REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS = 1000;
13467
13467
  const REMOTE_FLEET_BINARY_FRAME_STALE_FALLBACK_MS = 1400;
13468
13468
  const REMOTE_FLEET_BINARY_FRAME_CONNECT_GRACE_MS = 1600;
13469
+ const REMOTE_FLEET_BINARY_FRAME_CACHE_MS = 10000;
13470
+ const REMOTE_FLEET_BINARY_FRAME_CACHE_LIMIT = 240;
13469
13471
  const REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL = 8;
13470
13472
  const REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS = 3000;
13471
13473
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
@@ -13639,9 +13641,11 @@
13639
13641
  return nodeState;
13640
13642
  }
13641
13643
 
13642
- function clearRemoteFleetTimers(bodyView) {
13644
+ function clearRemoteFleetTimers(bodyView, options = {}) {
13643
13645
  if (!bodyView) return;
13644
- releaseRemoteFleetBinaryFrameSocket(bodyView);
13646
+ if (options.keepBinaryFrameSocket !== true) {
13647
+ releaseRemoteFleetBinaryFrameSocket(bodyView);
13648
+ }
13645
13649
  if (bodyView._remoteFleetLiveRefreshTimer) {
13646
13650
  clearInterval(bodyView._remoteFleetLiveRefreshTimer);
13647
13651
  bodyView._remoteFleetLiveRefreshTimer = null;
@@ -13822,6 +13826,57 @@
13822
13826
  .slice(0, 240);
13823
13827
  }
13824
13828
 
13829
+ function pruneRemoteFleetBinaryFrameCache(session) {
13830
+ if (!session?.latestBinaryFrames) {
13831
+ return;
13832
+ }
13833
+
13834
+ const now = getRemoteFleetFrameNow();
13835
+ for (const [deviceId, entry] of session.latestBinaryFrames) {
13836
+ if (!entry
13837
+ || !entry.frame
13838
+ || now - Number(entry.seenAt || 0) > REMOTE_FLEET_BINARY_FRAME_CACHE_MS
13839
+ || session.latestBinaryFrames.size > REMOTE_FLEET_BINARY_FRAME_CACHE_LIMIT) {
13840
+ session.latestBinaryFrames.delete(deviceId);
13841
+ }
13842
+ }
13843
+ }
13844
+
13845
+ function rememberRemoteFleetBinaryFrame(session, frame) {
13846
+ const deviceId = String(frame?.deviceId || '').trim();
13847
+ if (!session || !deviceId || !frame?._remoteFleetPayloadBlob) {
13848
+ return;
13849
+ }
13850
+
13851
+ if (!(session.latestBinaryFrames instanceof Map)) {
13852
+ session.latestBinaryFrames = new Map();
13853
+ }
13854
+
13855
+ pruneRemoteFleetBinaryFrameCache(session);
13856
+ session.latestBinaryFrames.set(deviceId, {
13857
+ seenAt: getRemoteFleetFrameNow(),
13858
+ frame: cloneRemoteFleetFramePatch(frame)
13859
+ });
13860
+ }
13861
+
13862
+ function applyRemoteFleetBinaryFrameCacheToBody(bodyView, session) {
13863
+ if (!bodyView || !(session?.latestBinaryFrames instanceof Map) || session.latestBinaryFrames.size === 0) {
13864
+ return 0;
13865
+ }
13866
+
13867
+ pruneRemoteFleetBinaryFrameCache(session);
13868
+ const patches = [];
13869
+ for (const entry of session.latestBinaryFrames.values()) {
13870
+ if (entry?.frame) {
13871
+ patches.push(cloneRemoteFleetFramePatch(entry.frame));
13872
+ }
13873
+ }
13874
+
13875
+ return patches.length > 0
13876
+ ? applyRemoteFleetFramePatches(bodyView, patches)
13877
+ : 0;
13878
+ }
13879
+
13825
13880
  function refreshRemoteFleetBinaryFrameSubscription(session, force = false) {
13826
13881
  if (!session?.ws || session.ws.readyState !== WebSocket.OPEN) {
13827
13882
  return false;
@@ -13877,7 +13932,11 @@
13877
13932
  refreshRemoteFleetBinaryFrameSubscription(session);
13878
13933
  return true;
13879
13934
  }
13935
+ if (session.wsOpening === true) {
13936
+ return true;
13937
+ }
13880
13938
 
13939
+ session.wsOpening = true;
13881
13940
  try {
13882
13941
  const status = await getRemoteFleetBridgeStatusForFrames();
13883
13942
  const wsUrl = buildRemoteFleetBinaryFrameWsUrl(status);
@@ -13891,6 +13950,7 @@
13891
13950
  session.subscriptionKey = '';
13892
13951
  ws.binaryType = 'arraybuffer';
13893
13952
  ws.onopen = () => {
13953
+ session.wsOpening = false;
13894
13954
  session.wsOpenedAt = getRemoteFleetFrameNow();
13895
13955
  window.RuntimeTrace?.emit?.('remote.frame.wsOpen', { nodeId: session.nodeId });
13896
13956
  refreshRemoteFleetBinaryFrameSubscription(session, true);
@@ -13922,6 +13982,7 @@
13922
13982
  let applied = 0;
13923
13983
  const frameAt = getRemoteFleetFrameNow();
13924
13984
  session.lastBinaryFrameAt = frameAt;
13985
+ rememberRemoteFleetBinaryFrame(session, frame);
13925
13986
  session.bodies.forEach(body => {
13926
13987
  if (document.body.contains(body)) {
13927
13988
  const bodyApplied = applyRemoteFleetFramePatches(body, [frame]);
@@ -13975,6 +14036,7 @@
13975
14036
  });
13976
14037
  };
13977
14038
  ws.onclose = () => {
14039
+ session.wsOpening = false;
13978
14040
  if (session.ws === ws) {
13979
14041
  session.ws = null;
13980
14042
  }
@@ -13990,6 +14052,7 @@
13990
14052
  };
13991
14053
  return true;
13992
14054
  } catch (error) {
14055
+ session.wsOpening = false;
13993
14056
  window.RuntimeTrace?.emit?.('remote.frame.wsOpenFailed', {
13994
14057
  nodeId: session.nodeId,
13995
14058
  error: error?.message || String(error || '')
@@ -14014,6 +14077,7 @@
14014
14077
  reconnectTimer: null,
14015
14078
  subscriptionTimer: null,
14016
14079
  subscriptionKey: '',
14080
+ latestBinaryFrames: new Map(),
14017
14081
  controlSessions: new Set(),
14018
14082
  getDeviceIds: null
14019
14083
  };
@@ -14031,6 +14095,7 @@
14031
14095
  mode: String(options.mode || 'remote-fast')
14032
14096
  };
14033
14097
  bodyView._remoteFleetBinaryFrameSession = session;
14098
+ applyRemoteFleetBinaryFrameCacheToBody(bodyView, session);
14034
14099
  openRemoteFleetBinaryFrameSocket(session).catch(() => undefined);
14035
14100
  return true;
14036
14101
  }
@@ -16043,7 +16108,7 @@
16043
16108
 
16044
16109
  function renderRemoteFleetDevice(bodyView, nodeModel) {
16045
16110
  if (!bodyView) return;
16046
- clearRemoteFleetTimers(bodyView);
16111
+ clearRemoteFleetTimers(bodyView, { keepBinaryFrameSocket: true });
16047
16112
 
16048
16113
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
16049
16114
  const device = parseRemoteFleetPinnedDevice(nodeModel);
@@ -16541,7 +16606,7 @@
16541
16606
 
16542
16607
  function renderRemoteFleetMonitor(bodyView, nodeModel) {
16543
16608
  if (!bodyView) return;
16544
- clearRemoteFleetTimers(bodyView);
16609
+ clearRemoteFleetTimers(bodyView, { keepBinaryFrameSocket: true });
16545
16610
 
16546
16611
  const nodeId = String(nodeModel?.id ?? nodeModel?.Id ?? '');
16547
16612
  const searchState = bodyView.dataset.remoteFleetSearch || '';
@@ -18038,11 +18103,14 @@
18038
18103
  clearRemoteFleetTimers(bodyView);
18039
18104
  }
18040
18105
  }, REMOTE_FLEET_LIVE_REFRESH_MS);
18041
- } else if (autoMonitorState && hasMonitorTargets) {
18042
- startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleFrames(true));
18043
- startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_MONITOR_REFRESH_MS, 'visible-devices');
18044
- } else if (autoMonitorState) {
18045
- startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS, 'empty-discovery');
18106
+ } else {
18107
+ releaseRemoteFleetBinaryFrameSocket(bodyView);
18108
+ if (autoMonitorState && hasMonitorTargets) {
18109
+ startRemoteFleetFrameLoop(bodyView, REMOTE_FLEET_THUMBNAIL_FRAME_REFRESH_MS, () => refreshRemoteFleetVisibleFrames(true));
18110
+ startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_MONITOR_REFRESH_MS, 'visible-devices');
18111
+ } else if (autoMonitorState) {
18112
+ startRemoteFleetMonitorRefreshTimer(REMOTE_FLEET_EMPTY_MONITOR_REFRESH_MS, 'empty-discovery');
18113
+ }
18046
18114
  }
18047
18115
  }
18048
18116
 
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "7WvF4J3m",
2
+ "version": "Q3LEcx0Y",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -86,7 +86,7 @@
86
86
  "url": "_content/MindExecution.Shared/js/mind-map-core.js.backup"
87
87
  },
88
88
  {
89
- "hash": "sha256-8Ndn0qAzhmRH7/OJtB0ve5LG2oN5e+PaAE7c0MyEzHM=",
89
+ "hash": "sha256-ct4yGB6E/ViQSpNNn3fHtt3v+usCW9LYQEbpOmzuRdk=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: 7WvF4J3m */
1
+ /* Manifest version: Q3LEcx0Y */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4