@mindexec/cli 0.2.445 → 0.2.447

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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -3
  3. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  4. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  5. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  6. package/remote-hub.js +171 -6
  7. package/scripts/remote-fast-live-rate-smoke.mjs +1 -0
  8. package/scripts/remote-fleet-render-smoke.mjs +74 -36
  9. package/scripts/remote-hub-smoke.mjs +7 -0
  10. package/server.js +2 -2
  11. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1 -3
  12. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +199 -100
  13. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -0
  14. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +186 -360
  15. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +124 -179
  16. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +167 -24
  17. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +96 -74
  18. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +66 -30
  19. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +6 -3
  20. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +35 -2
  21. package/wwwroot/_framework/{MindExecution.Core.4moacskf64.dll → MindExecution.Core.mswl4wlkm9.dll} +0 -0
  22. package/wwwroot/_framework/{MindExecution.Kernel.pa251cfsz4.dll → MindExecution.Kernel.zupeldfptg.dll} +0 -0
  23. package/wwwroot/_framework/{MindExecution.Plugins.Admin.dxyuwmxv1s.dll → MindExecution.Plugins.Admin.hq0vyqnqtn.dll} +0 -0
  24. package/wwwroot/_framework/{MindExecution.Plugins.Business.7uj5erqhhn.dll → MindExecution.Plugins.Business.pucw3o7rno.dll} +0 -0
  25. package/wwwroot/_framework/{MindExecution.Plugins.Concept.108rmsxc96.dll → MindExecution.Plugins.Concept.557tzzkrrx.dll} +0 -0
  26. package/wwwroot/_framework/{MindExecution.Plugins.Directory.ccsy3g0fe5.dll → MindExecution.Plugins.Directory.zh1ddg1fn6.dll} +0 -0
  27. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.1vjpuoe9ff.dll → MindExecution.Plugins.PlanMaster.0z9pzynpu9.dll} +0 -0
  28. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.q8ml98phj9.dll → MindExecution.Plugins.YouTube.y8g77r54fa.dll} +0 -0
  29. package/wwwroot/_framework/{MindExecution.Shared.2pywq19zjy.dll → MindExecution.Shared.1ymsdbwid2.dll} +0 -0
  30. package/wwwroot/_framework/{MindExecution.Web.qucbls1wfr.dll → MindExecution.Web.bb1txupuv0.dll} +0 -0
  31. package/wwwroot/_framework/blazor.boot.json +21 -21
  32. package/wwwroot/index.html +2 -1
  33. package/wwwroot/service-worker-assets.js +36 -32
  34. package/wwwroot/service-worker.js +1 -1
package/README.md CHANGED
@@ -52,7 +52,7 @@ experiments. By default it binds to loopback only:
52
52
 
53
53
  ```bash
54
54
  npx @mindexec/cli
55
- npx @mindexec/remote connect --manager 127.0.0.1:5197 --pair <pair-token>
55
+ npx @mindexec/remote connect --manager 127.0.0.1:5198 --pair <pair-token>
56
56
  ```
57
57
 
58
58
  Read the pair token from the protected local endpoint:
@@ -65,7 +65,7 @@ LAN/direct mode must be enabled explicitly by changing the RemoteHub bind host:
65
65
 
66
66
  ```powershell
67
67
  $env:REMOTE_HUB_HOST="0.0.0.0"
68
- $env:REMOTE_HUB_PORT="5197"
68
+ $env:REMOTE_HUB_PORT="5198"
69
69
  $env:REMOTE_HUB_PAIR_TOKEN="<strong-token>"
70
70
  npx @mindexec/cli
71
71
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.445",
3
+ "version": "0.2.447",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
66
- "@mindexec/remote": "^0.1.17",
66
+ "@mindexec/remote": "^0.1.18",
67
67
  "@openai/codex-sdk": "^0.144.1",
68
68
  "chokidar": "^3.6.0",
69
69
  "cors": "^2.8.5",
@@ -71,10 +71,13 @@
71
71
  "ffmpeg-static": "^5.3.0",
72
72
  "multer": "^2.2.0",
73
73
  "playwright-core": "^1.53.0",
74
- "sharp": "^0.33.2",
74
+ "sharp": "^0.35.3",
75
75
  "web-tree-sitter": "^0.22.6",
76
76
  "ws": "^8.16.0"
77
77
  },
78
+ "overrides": {
79
+ "body-parser": "1.20.6"
80
+ },
78
81
  "bin": {
79
82
  "mindexec": "launch-bridge.cjs",
80
83
  "mind-bridge": "launch-bridge.cjs"
package/remote-hub.js CHANGED
@@ -2,7 +2,7 @@ import net from 'net';
2
2
  import os from 'os';
3
3
  import crypto from 'crypto';
4
4
 
5
- const DEFAULT_REMOTE_HUB_PORT = 5197;
5
+ const DEFAULT_REMOTE_HUB_PORT = 5198;
6
6
  const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
7
7
  const DEFAULT_HEARTBEAT_MS = 5000;
8
8
  const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
@@ -212,6 +212,30 @@ function getSupportedRemoteFrameModeProfiles() {
212
212
  .map(serializeRemoteFrameModeProfile);
213
213
  }
214
214
 
215
+ function resolveDeviceRemoteFrameMode(device, requestedMode, fallbackMode = DEFAULT_REMOTE_FRAME_MODE) {
216
+ const requested = normalizeRemoteFrameMode(requestedMode, fallbackMode);
217
+ if (device?.frameModesAdvertised === false) {
218
+ return normalizeRemoteFrameMode(device?.frameProtocol?.defaultMode, DEFAULT_REMOTE_FRAME_MODE);
219
+ }
220
+
221
+ const supportedModes = new Set(
222
+ (Array.isArray(device?.frameModes) ? device.frameModes : [])
223
+ .filter(profile => profile?.implemented !== false)
224
+ .map(profile => normalizeRemoteFrameMode(profile?.mode || profile?.frameMode || profile?.profile, ''))
225
+ .filter(Boolean));
226
+
227
+ if (supportedModes.size === 0 || supportedModes.has(requested)) {
228
+ return requested;
229
+ }
230
+
231
+ const deviceDefault = normalizeRemoteFrameMode(device?.frameProtocol?.defaultMode, fallbackMode);
232
+ if (supportedModes.has(deviceDefault)) {
233
+ return deviceDefault;
234
+ }
235
+
236
+ return supportedModes.values().next().value || normalizeRemoteFrameMode(fallbackMode);
237
+ }
238
+
215
239
  function buildRemoteFrameProtocolDescriptor() {
216
240
  return {
217
241
  protocol: REMOTE_FRAME_PROTOCOL,
@@ -1125,6 +1149,12 @@ export function createRemoteHub(options = {}) {
1125
1149
  const taskBatches = new Map();
1126
1150
  const sockets = new Map();
1127
1151
  const allSockets = new Set();
1152
+ const sideChannelSockets = {
1153
+ input: new Map(),
1154
+ frame: new Map(),
1155
+ audio: new Map(),
1156
+ file: new Map()
1157
+ };
1128
1158
  const duplicateDeviceLogAt = new Map();
1129
1159
  let server = null;
1130
1160
  let started = false;
@@ -1523,6 +1553,9 @@ export function createRemoteHub(options = {}) {
1523
1553
 
1524
1554
  function closeExistingDeviceSocket(deviceId, nextSessionId) {
1525
1555
  const existing = devices.get(deviceId);
1556
+ for (const channel of Object.keys(sideChannelSockets)) {
1557
+ closeSideChannel(existing, channel, 'replaced-by-new-session');
1558
+ }
1526
1559
  if (!existing?.socket || existing.socket.destroyed) {
1527
1560
  return;
1528
1561
  }
@@ -1537,6 +1570,37 @@ export function createRemoteHub(options = {}) {
1537
1570
  existing.socket.destroy();
1538
1571
  }
1539
1572
 
1573
+ function closeSideChannel(device, channel, reason = `${channel}-socket-closed`) {
1574
+ const socketKey = `${channel}Socket`;
1575
+ const socket = device?.[socketKey];
1576
+ if (!socket) return;
1577
+ device[socketKey] = null;
1578
+ sideChannelSockets[channel]?.delete(socket);
1579
+ try {
1580
+ writeJsonLine(socket, { type: 'disconnect', channel, reason });
1581
+ } catch {
1582
+ // Best-effort notice before closing the side channel.
1583
+ }
1584
+ try {
1585
+ socket.destroy?.();
1586
+ } catch {
1587
+ // Ignore close failures.
1588
+ }
1589
+ }
1590
+
1591
+ function detachSideChannel(socket, channel, reason = `${channel}-socket-closed`) {
1592
+ const channelMap = sideChannelSockets[channel];
1593
+ const deviceId = channelMap?.get(socket);
1594
+ channelMap?.delete(socket);
1595
+ if (!deviceId) return;
1596
+ const device = devices.get(deviceId);
1597
+ const socketKey = `${channel}Socket`;
1598
+ if (!device || device[socketKey] !== socket) return;
1599
+ device[socketKey] = null;
1600
+ device[`${channel}LastSeenAt`] = new Date().toISOString();
1601
+ emitRemoteEvent(`Remote${channel[0].toUpperCase()}${channel.slice(1)}SocketDisconnected`, device, { reason });
1602
+ }
1603
+
1540
1604
  function getDeviceActivityMs(device) {
1541
1605
  return Math.max(
1542
1606
  Date.parse(device?.lastSeenAt || '') || 0,
@@ -1841,6 +1905,7 @@ export function createRemoteHub(options = {}) {
1841
1905
  protocolVersion: Number.isFinite(Number(hello.protocolVersion)) ? Math.max(1, Math.floor(Number(hello.protocolVersion))) : 1,
1842
1906
  frameProtocol: frameProtocol || buildRemoteFrameProtocolDescriptor(),
1843
1907
  frameModes: frameModes.length > 0 ? frameModes : getSupportedRemoteFrameModeProfiles(),
1908
+ frameModesAdvertised: frameModes.length > 0,
1844
1909
  capabilities,
1845
1910
  connected: true,
1846
1911
  connectedAt: now,
@@ -1853,6 +1918,10 @@ export function createRemoteHub(options = {}) {
1853
1918
  status: {},
1854
1919
  latestThumbnail: null,
1855
1920
  latestLiveFrame: null,
1921
+ inputSocket: null,
1922
+ frameSocket: null,
1923
+ audioSocket: null,
1924
+ fileSocket: null,
1856
1925
  recentFramePayloads: {
1857
1926
  thumbnail: [],
1858
1927
  live: []
@@ -1885,6 +1954,40 @@ export function createRemoteHub(options = {}) {
1885
1954
  return device;
1886
1955
  }
1887
1956
 
1957
+ function attachSideChannel(socket, hello, channel) {
1958
+ const deviceId = normalizeDeviceId(hello.deviceId);
1959
+ const device = devices.get(deviceId);
1960
+ if (!device || !device.connected || !device.socket || device.socket.destroyed) {
1961
+ writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
1962
+ socket.destroy();
1963
+ return null;
1964
+ }
1965
+
1966
+ closeSideChannel(device, channel, `replaced-by-new-${channel}-socket`);
1967
+ const now = new Date().toISOString();
1968
+ device[`${channel}Socket`] = socket;
1969
+ device[`${channel}ConnectedAt`] = now;
1970
+ device[`${channel}LastSeenAt`] = now;
1971
+ sideChannelSockets[channel].set(socket, deviceId);
1972
+ writeJsonLine(socket, {
1973
+ type: 'welcome',
1974
+ channel,
1975
+ protocol: REMOTE_AGENT_PROTOCOL,
1976
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
1977
+ sessionId: device.sessionId,
1978
+ deviceId,
1979
+ ...(channel === 'frame'
1980
+ ? {
1981
+ frameProtocol: buildRemoteFrameProtocolDescriptor(),
1982
+ frameModes: getSupportedRemoteFrameModeProfiles()
1983
+ }
1984
+ : {}),
1985
+ serverTime: now
1986
+ });
1987
+ emitRemoteEvent(`Remote${channel[0].toUpperCase()}${channel.slice(1)}SocketConnected`, device);
1988
+ return device;
1989
+ }
1990
+
1888
1991
  function detachSocket(socket, reason = 'socket-closed') {
1889
1992
  const deviceId = sockets.get(socket);
1890
1993
  sockets.delete(socket);
@@ -1906,6 +2009,9 @@ export function createRemoteHub(options = {}) {
1906
2009
  device.activeLiveStream.stoppedAt = device.disconnectedAt;
1907
2010
  device.activeLiveStream.stopReason = reason;
1908
2011
  }
2012
+ for (const channel of Object.keys(sideChannelSockets)) {
2013
+ closeSideChannel(device, channel, reason);
2014
+ }
1909
2015
  failAllPendingTasks(device, 'device-disconnected');
1910
2016
  logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
1911
2017
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
@@ -2570,6 +2676,16 @@ export function createRemoteHub(options = {}) {
2570
2676
  return;
2571
2677
  }
2572
2678
 
2679
+ const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
2680
+ if (Object.hasOwn(sideChannelSockets, channel)) {
2681
+ const device = attachSideChannel(socket, message, channel);
2682
+ if (!device) return;
2683
+ state.authenticated = true;
2684
+ state.device = device;
2685
+ state.sideChannel = channel;
2686
+ return;
2687
+ }
2688
+
2573
2689
  const device = attachDevice(socket, message);
2574
2690
  if (!device) {
2575
2691
  return;
@@ -2586,6 +2702,17 @@ export function createRemoteHub(options = {}) {
2586
2702
  return;
2587
2703
  }
2588
2704
 
2705
+ if (state.sideChannel) {
2706
+ device[`${state.sideChannel}LastSeenAt`] = new Date().toISOString();
2707
+ if (state.sideChannel === 'input' && message.type === 'input.error') {
2708
+ emitRemoteEvent('RemoteInputError', device, {
2709
+ inputSeq: Number(message.inputSeq || 0) || 0,
2710
+ error: safeString(message.error, 600) || 'remote-input-failed'
2711
+ });
2712
+ }
2713
+ return;
2714
+ }
2715
+
2589
2716
  device.counters.messagesReceived += 1;
2590
2717
  device.lastSeenAt = new Date().toISOString();
2591
2718
 
@@ -2698,7 +2825,11 @@ export function createRemoteHub(options = {}) {
2698
2825
  const detach = reason => {
2699
2826
  allSockets.delete(socket);
2700
2827
  clearTimeout(helloTimer);
2701
- detachSocket(socket, reason);
2828
+ if (state.sideChannel) {
2829
+ detachSideChannel(socket, state.sideChannel, reason);
2830
+ } else {
2831
+ detachSocket(socket, reason);
2832
+ }
2702
2833
  };
2703
2834
 
2704
2835
  socket.onCloseMessage = reason => detach(reason || 'websocket-closed');
@@ -2867,12 +2998,20 @@ export function createRemoteHub(options = {}) {
2867
2998
  socket.on('close', () => {
2868
2999
  allSockets.delete(socket);
2869
3000
  clearTimeout(helloTimer);
2870
- detachSocket(socket);
3001
+ if (state.sideChannel) {
3002
+ detachSideChannel(socket, state.sideChannel);
3003
+ } else {
3004
+ detachSocket(socket);
3005
+ }
2871
3006
  });
2872
3007
  socket.on('error', err => {
2873
3008
  allSockets.delete(socket);
2874
3009
  clearTimeout(helloTimer);
2875
- detachSocket(socket, err?.message || 'socket-error');
3010
+ if (state.sideChannel) {
3011
+ detachSideChannel(socket, state.sideChannel, err?.message || 'socket-error');
3012
+ } else {
3013
+ detachSocket(socket, err?.message || 'socket-error');
3014
+ }
2876
3015
  });
2877
3016
  }
2878
3017
 
@@ -2936,6 +3075,7 @@ export function createRemoteHub(options = {}) {
2936
3075
  }
2937
3076
 
2938
3077
  sockets.clear();
3078
+ Object.values(sideChannelSockets).forEach(channelMap => channelMap.clear());
2939
3079
  if (!server) {
2940
3080
  started = false;
2941
3081
  return;
@@ -3058,10 +3198,29 @@ export function createRemoteHub(options = {}) {
3058
3198
  return { ok: false, error: 'missing-input-type' };
3059
3199
  }
3060
3200
 
3201
+ const inputSocket = device.inputSocket;
3202
+ if (inputSocket && !inputSocket.destroyed) {
3203
+ const sent = writeJsonLine(inputSocket, {
3204
+ type: 'input.control',
3205
+ payload: {
3206
+ ...normalized,
3207
+ hubForwardedAtEpochMs: Date.now(),
3208
+ issuedAt: normalized.issuedAt || new Date().toISOString()
3209
+ }
3210
+ });
3211
+ if (sent) {
3212
+ device.counters.commandsSent += 1;
3213
+ device.inputLastSeenAt = new Date().toISOString();
3214
+ return { ok: true, inputSocket: true };
3215
+ }
3216
+ detachSideChannel(inputSocket, 'input', 'input-socket-write-failed');
3217
+ }
3218
+
3061
3219
  return sendCommand(deviceId, {
3062
3220
  command: 'input.control',
3063
3221
  payload: {
3064
3222
  ...normalized,
3223
+ hubForwardedAtEpochMs: Date.now(),
3065
3224
  issuedAt: normalized.issuedAt || new Date().toISOString()
3066
3225
  }
3067
3226
  });
@@ -3285,7 +3444,10 @@ export function createRemoteHub(options = {}) {
3285
3444
  const now = new Date().toISOString();
3286
3445
  const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
3287
3446
  const streamPurpose = safeString(options.streamPurpose, 32).toLowerCase();
3288
- const requestedMode = options.mode || (streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
3447
+ const requestedMode = resolveDeviceRemoteFrameMode(
3448
+ device,
3449
+ options.mode || (streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo'),
3450
+ streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
3289
3451
  const fps = clampNumber(options.fps, 1, 30, streamPurpose === 'control' ? 30 : 20);
3290
3452
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
3291
3453
  const transfer = buildRemoteFrameTransferDescriptor(requestedMode, streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
@@ -3342,7 +3504,10 @@ export function createRemoteHub(options = {}) {
3342
3504
  const now = new Date().toISOString();
3343
3505
  const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
3344
3506
  const streamPurpose = safeString(options.streamPurpose, 32).toLowerCase();
3345
- const requestedMode = options.mode || (streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
3507
+ const requestedMode = resolveDeviceRemoteFrameMode(
3508
+ device,
3509
+ options.mode || (streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo'),
3510
+ streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
3346
3511
  const fps = clampNumber(options.fps, 1, 30, streamPurpose === 'control' ? 30 : 20);
3347
3512
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
3348
3513
  const transfer = buildRemoteFrameTransferDescriptor(requestedMode, streamPurpose === 'control' ? 'mode3-h264-hw' : 'mode2-lzo');
@@ -293,6 +293,7 @@ async function main() {
293
293
  const live = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
294
294
  method: 'POST',
295
295
  body: JSON.stringify({
296
+ mode: 'remote-fast',
296
297
  fps: REQUESTED_FPS,
297
298
  maxWidth: 960,
298
299
  maxHeight: 540,
@@ -435,7 +435,9 @@ function projectDevice(device) {
435
435
 
436
436
  async function loadCss3DManager() {
437
437
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
438
+ const hotPathModulePath = path.resolve(__dirname, '../../MindExecution.Shared/wwwroot/js/mind-map-css3d-hotpath.js');
438
439
  const managerPath = path.resolve(__dirname, '../../MindExecution.Shared/wwwroot/js/mind-map-css3d-manager.js');
440
+ const hotPathSource = await fs.readFile(hotPathModulePath, 'utf8');
439
441
  const source = await fs.readFile(managerPath, 'utf8');
440
442
  const document = new MiniDocument();
441
443
  const objectUrlCalls = [];
@@ -558,11 +560,18 @@ async function loadCss3DManager() {
558
560
  textOverlayInvalidations.push({ module, nodeId, reason, fallback: true });
559
561
  }
560
562
  },
563
+ MindExecRemoteCodecs: {
564
+ async decodeH264Frame(canvas, frame, onFrame) {
565
+ onFrame?.(canvas, frame);
566
+ return true;
567
+ }
568
+ },
561
569
  Promise
562
570
  };
563
571
  context.window = context;
564
572
  context.self = context;
565
573
 
574
+ vm.runInNewContext(hotPathSource, context, { filename: hotPathModulePath });
566
575
  vm.runInNewContext(source, context, { filename: managerPath });
567
576
  return {
568
577
  manager: context.window.MindMapCss3DManager,
@@ -584,6 +593,15 @@ function wait(ms = 0) {
584
593
  return new Promise(resolve => setTimeout(resolve, ms));
585
594
  }
586
595
 
596
+ async function waitFor(predicate, timeoutMs = 500, intervalMs = 10) {
597
+ const deadline = Date.now() + Math.max(1, Number(timeoutMs || 500));
598
+ while (Date.now() < deadline) {
599
+ if (predicate()) return true;
600
+ await wait(intervalMs);
601
+ }
602
+ return predicate();
603
+ }
604
+
587
605
  function encodeRemoteBinaryFrame(metadata, payload = new Uint8Array([0xff, 0xd8, 0xff, 0xd9])) {
588
606
  const metaBytes = new TextEncoder().encode(JSON.stringify(metadata));
589
607
  const payloadBytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
@@ -598,7 +616,7 @@ function encodeRemoteBinaryFrame(metadata, payload = new Uint8Array([0xff, 0xd8,
598
616
 
599
617
  function buildMonitorNode(devices, hubStatus, latestTaskBatch = null, recentTaskBatches = [], lastError = '', nodeId = 'remote-fleet-render-smoke') {
600
618
  const connected = devices.filter(device => device.Connected).length;
601
- const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
619
+ const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5198';
602
620
  const pairToken = hubStatus.pairToken || 'render-smoke-token';
603
621
  const hostTargetState = hubStatus.hostTargetActive
604
622
  ? (hubStatus.hostTargetNodeId === nodeId ? 'hosting' : 'other-monitor')
@@ -665,7 +683,7 @@ function createRemoteFleetTemplateShell(document, focusDeviceId = '', nodeId = '
665
683
  }
666
684
 
667
685
  function buildDeviceNode(device, hubStatus) {
668
- const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5197';
686
+ const endpoint = hubStatus.agentEndpoint || '127.0.0.1:5198';
669
687
  return {
670
688
  id: 'remote-device-render-smoke',
671
689
  contentType: 'memo',
@@ -1060,6 +1078,13 @@ try {
1060
1078
  binaryDrawCalls.some(call => call.op === 'clearRect' || call.op === 'fillRect'),
1061
1079
  false,
1062
1080
  'binary Blob frame paint must not clear/fill the canvas before drawing');
1081
+ assert.ok(
1082
+ Number(moduleRef._forceUpdateFrames || 0) >= 2,
1083
+ 'binary live frame commits must wake the passive MDM CSS3D frame loop');
1084
+ assert.equal(moduleRef._lastAnimationWakeReason, 'remote-live-css3d-frame');
1085
+ assert.ok(
1086
+ diagnostics.runtimeTrace.some(event => event.type === 'remote.frame.css3dWake'),
1087
+ 'binary live frame commits must trace the MDM CSS3D wake path');
1063
1088
  const binaryCanvasBeforeRerender = binaryCanvas;
1064
1089
 
1065
1090
  const liveOverlayCard = document.createElement('div');
@@ -1073,12 +1098,28 @@ try {
1073
1098
  moduleRef._lastAnimationWakeReason = '';
1074
1099
 
1075
1100
  await wait(20);
1076
- const liveWs = diagnostics.webSocketConnections[0];
1077
- assert.ok(liveWs, 'expected MDM render to open the binary frame WebSocket');
1078
- assert.equal(liveWs.readyState, 1, 'binary frame WebSocket should be open before rerender');
1101
+ const liveWs = diagnostics.webSocketConnections.find(connection =>
1102
+ String(connection?.url || '').includes('/api/remote/atlas/ws'));
1103
+ assert.ok(
1104
+ liveWs,
1105
+ `expected MDM render to open the Mode 4 Atlas WebSocket: ${diagnostics.webSocketConnections.map(connection => connection.url).join(', ')}`
1106
+ );
1107
+ assert.equal(liveWs.readyState, 1, 'Mode 4 Atlas WebSocket should be open before rerender');
1079
1108
  const wsCountBeforeRerender = diagnostics.webSocketConnections.length;
1080
1109
  const wsCloseCountBeforeRerender = liveWs.closeCount;
1081
1110
  const websocketFrameSeq = 10001;
1111
+ liveWs.onmessage?.({
1112
+ data: JSON.stringify({
1113
+ type: 'Mode4AtlasLayout',
1114
+ tiles: [{
1115
+ deviceId: patchTarget.DeviceId,
1116
+ x: 0,
1117
+ y: 0,
1118
+ width: 64,
1119
+ height: 36
1120
+ }]
1121
+ })
1122
+ });
1082
1123
  liveWs.onmessage?.({
1083
1124
  data: new Blob([encodeRemoteBinaryFrame({
1084
1125
  type: 'remote.frame.binary',
@@ -1088,36 +1129,36 @@ try {
1088
1129
  streamId: 'render-smoke-ws-live',
1089
1130
  contentHash: `render-smoke-ws-live-${websocketFrameSeq}`,
1090
1131
  mimeType: 'image/jpeg',
1132
+ frameMode: 'mode4-h264-atlas',
1091
1133
  width: 64,
1092
1134
  height: 36,
1093
1135
  capturedAt: new Date().toISOString(),
1094
1136
  receivedAt: new Date().toISOString()
1095
1137
  })])
1096
1138
  });
1097
- await wait(30);
1139
+ await waitFor(
1140
+ () => patchPreview.dataset.remoteFleetFrameSeq === String(websocketFrameSeq)
1141
+ );
1098
1142
  assert.equal(
1099
1143
  patchPreview.dataset.remoteFleetFrameSeq,
1100
1144
  String(websocketFrameSeq),
1101
1145
  JSON.stringify(diagnostics.runtimeTrace.slice(-12)));
1102
- await wait(10);
1103
- assert.ok(
1104
- Number(moduleRef._forceUpdateFrames || 0) >= 2,
1105
- 'binary live frame commits must wake the passive MDM CSS3D frame loop');
1106
- assert.equal(moduleRef._lastAnimationWakeReason, 'remote-live-css3d-frame');
1107
1146
  assert.ok(
1108
- diagnostics.runtimeTrace.some(event => event.type === 'remote.frame.css3dWake'),
1109
- 'binary live frame commits must trace the MDM CSS3D wake path');
1147
+ diagnostics.runtimeTrace.some(event =>
1148
+ event.type === 'remote.atlas.painted' &&
1149
+ Number(event.frameSeq || 0) === websocketFrameSeq),
1150
+ 'Mode 4 Atlas frame must paint its configured device tile');
1110
1151
 
1111
1152
  manager.renderRemoteFleetMonitorForTest(bodyView, buildMonitorNode(devices, hub.getStatus({ includeSecrets: true }), latestTaskBatch, recentTaskBatches));
1112
1153
  await wait(30);
1113
1154
  assert.equal(
1114
1155
  liveWs.closeCount,
1115
1156
  wsCloseCountBeforeRerender,
1116
- 'MDM rerender must not close the active binary frame WebSocket');
1157
+ 'MDM rerender must not close the active Mode 4 Atlas WebSocket');
1117
1158
  assert.equal(
1118
1159
  diagnostics.webSocketConnections.length,
1119
1160
  wsCountBeforeRerender,
1120
- 'MDM rerender must reuse the active binary frame WebSocket instead of opening a replacement');
1161
+ 'MDM rerender must reuse the active Mode 4 Atlas WebSocket instead of opening a replacement');
1121
1162
  const rerenderedPatchPreview = bodyView
1122
1163
  .querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`)
1123
1164
  ?.querySelector('[data-remote-fleet-device-preview="tile"]');
@@ -1148,15 +1189,11 @@ try {
1148
1189
  .querySelector(`article[data-device-id="${patchTarget.DeviceId}"]`)
1149
1190
  ?.querySelector('[data-remote-fleet-device-preview="tile"]');
1150
1191
  assert.ok(otherBoardPatchPreview);
1151
- assert.equal(
1152
- otherBoardPatchPreview.dataset.remoteFleetFrameSeq,
1153
- String(websocketFrameSeq),
1154
- 'a different-board MDM should prime from the device-global latest binary frame cache');
1155
- assert.equal(
1156
- otherBoardPatchPreview.querySelector('canvas[data-remote-fleet-frame-canvas="true"]')?.style.display,
1157
- 'block');
1192
+ assert.ok(
1193
+ diagnostics.webSocketConnections.length > wsCountBeforeRerender,
1194
+ 'a different-board MDM must own a separate Mode 4 Atlas session');
1158
1195
  assert.ok(diagnostics.runtimeTrace.some(event =>
1159
- (event.type === 'remote.frame.wsCachePrimed' || event.type === 'remote.frame.surfaceCachePrimed')
1196
+ event.type === 'remote.atlas.wsOpen'
1160
1197
  && event.nodeId === otherBoardNodeId));
1161
1198
  otherBoardShell.remove();
1162
1199
 
@@ -1260,18 +1297,17 @@ try {
1260
1297
  }
1261
1298
  })
1262
1299
  .filter(Boolean);
1263
- const autoLiveSubscribe = subscribeMessages.find(message =>
1264
- message.type === 'subscribe'
1265
- && message.nodeId === 'remote-fleet-render-smoke'
1266
- && message.autoStartLive === true);
1300
+ const atlasConfigure = subscribeMessages.find(message =>
1301
+ message.type === 'configure');
1267
1302
  assert.ok(diagnostics.fetchCalls.some(call => call.url.includes('/api/status?remoteFrames=ws')));
1268
- assert.ok(diagnostics.webSocketConnections.length >= 1, 'expected MDM render to open binary frame WebSocket');
1269
- assert.ok(autoLiveSubscribe, 'expected MDM render to subscribe with autoStartLive over WebSocket');
1270
- assert.equal(autoLiveSubscribe.fps, 12);
1271
- assert.equal(autoLiveSubscribe.maxWidth, 960);
1272
- assert.equal(autoLiveSubscribe.maxHeight, 540);
1273
- assert.equal(autoLiveSubscribe.quality, 60);
1274
- assert.ok(autoLiveSubscribe.deviceIds.length > 1, 'expected visible connected devices in WS subscription');
1303
+ assert.ok(diagnostics.webSocketConnections.length >= 1, 'expected MDM render to open Mode 4 Atlas WebSocket');
1304
+ assert.ok(atlasConfigure, 'expected MDM render to configure the Mode 4 Atlas WebSocket');
1305
+ assert.equal(atlasConfigure.fps, 20);
1306
+ assert.equal(atlasConfigure.width, 1920);
1307
+ assert.equal(atlasConfigure.height, 1080);
1308
+ assert.equal(atlasConfigure.tileWidth, 320);
1309
+ assert.equal(atlasConfigure.tileHeight, 180);
1310
+ assert.ok(atlasConfigure.deviceIds.length > 1, 'expected visible connected devices in Atlas configuration');
1275
1311
  assert.ok(diagnostics.runtimeTrace.some(event => event.type === 'remote.live.wsAutoStartRequested'));
1276
1312
  assert.equal(liveStartCalls.length, 0, 'WebSocket live path must not call DotNet live-start fallback');
1277
1313
  await wait(320);
@@ -1373,10 +1409,12 @@ try {
1373
1409
  && message.type === 'subscribe'
1374
1410
  && message.nodeId === 'remote-device-render-smoke'
1375
1411
  && message.autoStartLive === true
1376
- && message.fps === 20
1412
+ && message.fps === 30
1413
+ && message.mode === 'mode3-h264-hw'
1414
+ && message.streamPurpose === 'control'
1377
1415
  && Array.isArray(message.deviceIds)
1378
1416
  && message.deviceIds.includes(focusedDevice.DeviceId));
1379
- assert.ok(deviceControlSubscribe, 'RemoteFleetDevice node should subscribe to 20fps binary frames');
1417
+ assert.ok(deviceControlSubscribe, 'RemoteFleetDevice node should subscribe to 30fps Mode 3 control frames');
1380
1418
  const deviceControlWs = diagnostics.webSocketConnections
1381
1419
  .filter(ws => ws.url.includes('/api/remote/frames/ws'))
1382
1420
  .at(-1);
@@ -22,6 +22,13 @@ function wait(ms) {
22
22
  return new Promise(resolve => setTimeout(resolve, ms));
23
23
  }
24
24
 
25
+ const defaultHub = createRemoteHub({
26
+ env: {
27
+ MINDEXEC_REMOTE_HUB: '0'
28
+ }
29
+ });
30
+ assert.equal(defaultHub.getStatus().port, 5198, 'MindExecution RemoteHub must not reuse the LiveDesk 5197 port');
31
+
25
32
  async function waitFor(predicate, timeoutMs = 3000) {
26
33
  const startedAt = Date.now();
27
34
  while (Date.now() - startedAt < timeoutMs) {
package/server.js CHANGED
@@ -4354,7 +4354,7 @@ function appendRemoteAgentOutput(stream, chunk, stateConnectionKey = '') {
4354
4354
  const text = chunk.toString();
4355
4355
  const key = stream === 'stderr' ? 'stderrTail' : 'stdoutTail';
4356
4356
  remoteAgentState[key] = String((remoteAgentState[key] || '') + text).slice(-REMOTE_AGENT_STDIO_TAIL_CHARS);
4357
- if (/Connected to RemoteHub/i.test(text)) {
4357
+ if (/Connected to (?:RemoteHub|LiveDesk Hub)/i.test(text)) {
4358
4358
  remoteAgentState.ready = true;
4359
4359
  remoteAgentState.connectedAt = new Date().toISOString();
4360
4360
  remoteAgentState.needsReconnect = false;
@@ -5222,7 +5222,7 @@ function appendRemoteAgentAttemptOutput(attempt, stream, chunk) {
5222
5222
  const text = chunk.toString();
5223
5223
  const key = stream === 'stderr' ? 'stderrTail' : 'stdoutTail';
5224
5224
  attempt.state[key] = String((attempt.state[key] || '') + text).slice(-REMOTE_AGENT_STDIO_TAIL_CHARS);
5225
- if (/Connected to RemoteHub/i.test(text)) {
5225
+ if (/Connected to (?:RemoteHub|LiveDesk Hub)/i.test(text)) {
5226
5226
  attempt.state.ready = true;
5227
5227
  attempt.state.connectedAt = new Date().toISOString();
5228
5228
  attempt.state.needsReconnect = false;
@@ -302,10 +302,8 @@
302
302
  themeGroup.userData.localSnapGridStyle = options.localSnapGridStyle || 'line';
303
303
  themeGroup.userData.localSnapGridRadiusPx = 500;
304
304
  themeGroup.userData.themeAlias = options.themeAlias || 'LocalSnapGrid';
305
+ themeGroup.userData.localSnapGridBackgroundColor = options.backgroundColor;
305
306
  themeGroup.animate = () => { };
306
- if (scene && options.backgroundColor !== undefined) {
307
- scene.background = new THREE.Color(options.backgroundColor);
308
- }
309
307
  return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.None);
310
308
  }
311
309