@mindexec/cli 0.2.394 → 0.2.396

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.394",
3
+ "version": "0.2.396",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -20,7 +20,73 @@ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
20
20
  const RECENT_LIVE_FRAME_CACHE_LIMIT = 80;
21
21
  const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
22
22
  const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
23
- const REMOTE_PROTOCOL_VERSION = 1;
23
+ const REMOTE_PROTOCOL_VERSION = 2;
24
+ const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
25
+ const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
26
+ const REMOTE_FRAME_PROTOCOL_VERSION = 2;
27
+ const REMOTE_FRAME_HANDSHAKE = 'smartview-request-open-frame';
28
+ const DEFAULT_REMOTE_FRAME_MODE = 'remote-fast';
29
+ const REMOTE_FRAME_MODE_PROFILES = Object.freeze({
30
+ thumbnail: Object.freeze({
31
+ mode: 'thumbnail',
32
+ label: 'Thumbnail PNG/JPEG Binary',
33
+ transport: 'ws-binary',
34
+ encoding: 'image',
35
+ codec: 'image',
36
+ compression: 'image/*',
37
+ profile: 'thumbnail-binary-v1',
38
+ modeVersion: 1,
39
+ implemented: true
40
+ }),
41
+ 'remote-fast': Object.freeze({
42
+ mode: 'remote-fast',
43
+ label: 'RemoteFast JPEG Binary',
44
+ transport: 'ws-binary',
45
+ encoding: 'image',
46
+ codec: 'jpeg',
47
+ compression: 'image/jpeg',
48
+ profile: 'jpeg-binary-v1',
49
+ modeVersion: 1,
50
+ implemented: true
51
+ }),
52
+ 'remote-quality': Object.freeze({
53
+ mode: 'remote-quality',
54
+ label: 'RemoteQuality JPEG Binary',
55
+ transport: 'ws-binary',
56
+ encoding: 'image',
57
+ codec: 'jpeg',
58
+ compression: 'image/jpeg',
59
+ profile: 'jpeg-quality-binary-v1',
60
+ modeVersion: 1,
61
+ implemented: true
62
+ }),
63
+ 'video-codec': Object.freeze({
64
+ mode: 'video-codec',
65
+ label: 'Video Codec Stream',
66
+ transport: 'ws-binary',
67
+ encoding: 'video',
68
+ codec: 'video',
69
+ compression: 'video/*',
70
+ profile: 'future-video-codec',
71
+ modeVersion: 0,
72
+ implemented: false
73
+ })
74
+ });
75
+ const REMOTE_FRAME_MODE_ALIASES = new Map([
76
+ ['fast', 'remote-fast'],
77
+ ['remotefast', 'remote-fast'],
78
+ ['remote_fast', 'remote-fast'],
79
+ ['remote-fast', 'remote-fast'],
80
+ ['quality', 'remote-quality'],
81
+ ['remotequality', 'remote-quality'],
82
+ ['remote_quality', 'remote-quality'],
83
+ ['remote-quality', 'remote-quality'],
84
+ ['thumb', 'thumbnail'],
85
+ ['thumbnail', 'thumbnail'],
86
+ ['video', 'video-codec'],
87
+ ['codec', 'video-codec'],
88
+ ['video-codec', 'video-codec']
89
+ ]);
24
90
  const MAX_SYNTHETIC_DEVICES = 1000;
25
91
  const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
26
92
  const DUPLICATE_DEVICE_ACTIVE_REJECT_MS = 15000;
@@ -58,6 +124,115 @@ function safeString(value, maxLength = 200) {
58
124
  return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
59
125
  }
60
126
 
127
+ function normalizeRemoteFrameMode(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
128
+ const requested = safeString(value, 80).toLowerCase();
129
+ const fallbackMode = REMOTE_FRAME_MODE_PROFILES[fallback] ? fallback : DEFAULT_REMOTE_FRAME_MODE;
130
+ if (!requested) {
131
+ return fallbackMode;
132
+ }
133
+
134
+ const normalized = REMOTE_FRAME_MODE_ALIASES.get(requested) || requested;
135
+ const profile = REMOTE_FRAME_MODE_PROFILES[normalized];
136
+ if (!profile || profile.implemented !== true) {
137
+ return fallbackMode;
138
+ }
139
+
140
+ return normalized;
141
+ }
142
+
143
+ function serializeRemoteFrameModeProfile(profile) {
144
+ return {
145
+ mode: profile.mode,
146
+ label: profile.label,
147
+ transport: profile.transport,
148
+ encoding: profile.encoding,
149
+ codec: profile.codec,
150
+ compression: profile.compression,
151
+ profile: profile.profile,
152
+ modeVersion: profile.modeVersion,
153
+ implemented: profile.implemented === true
154
+ };
155
+ }
156
+
157
+ function getRemoteFrameModeProfile(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
158
+ const mode = normalizeRemoteFrameMode(value, fallback);
159
+ return serializeRemoteFrameModeProfile(REMOTE_FRAME_MODE_PROFILES[mode]);
160
+ }
161
+
162
+ function getSupportedRemoteFrameModeProfiles() {
163
+ return Object.values(REMOTE_FRAME_MODE_PROFILES)
164
+ .filter(profile => profile.implemented === true)
165
+ .map(serializeRemoteFrameModeProfile);
166
+ }
167
+
168
+ function buildRemoteFrameProtocolDescriptor() {
169
+ return {
170
+ protocol: REMOTE_FRAME_PROTOCOL,
171
+ version: REMOTE_FRAME_PROTOCOL_VERSION,
172
+ handshake: REMOTE_FRAME_HANDSHAKE,
173
+ defaultMode: DEFAULT_REMOTE_FRAME_MODE,
174
+ modes: getSupportedRemoteFrameModeProfiles()
175
+ };
176
+ }
177
+
178
+ function buildRemoteFrameTransferDescriptor(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
179
+ const profile = getRemoteFrameModeProfile(value, fallback);
180
+ return {
181
+ mode: profile.mode,
182
+ frameMode: profile.mode,
183
+ frameProfile: profile.profile,
184
+ encoding: profile.encoding,
185
+ codec: profile.codec,
186
+ compression: profile.compression,
187
+ transferProtocol: REMOTE_FRAME_PROTOCOL,
188
+ transferProtocolVersion: REMOTE_FRAME_PROTOCOL_VERSION,
189
+ handshake: REMOTE_FRAME_HANDSHAKE
190
+ };
191
+ }
192
+
193
+ function buildRemoteFrameTransferDescriptorFromMessage(message = {}, fallback = DEFAULT_REMOTE_FRAME_MODE) {
194
+ const descriptor = buildRemoteFrameTransferDescriptor(message.frameMode || message.mode, fallback);
195
+ return {
196
+ ...descriptor,
197
+ frameProfile: safeString(message.frameProfile, 80) || descriptor.frameProfile,
198
+ encoding: safeString(message.encoding, 40) || descriptor.encoding,
199
+ codec: safeString(message.codec, 40) || descriptor.codec,
200
+ compression: safeString(message.compression || message.mimeType || message.format, 80) || descriptor.compression,
201
+ transferProtocol: safeString(message.transferProtocol, 80) || descriptor.transferProtocol,
202
+ transferProtocolVersion: Number.isFinite(Number(message.transferProtocolVersion))
203
+ ? Math.max(1, Math.floor(Number(message.transferProtocolVersion)))
204
+ : descriptor.transferProtocolVersion,
205
+ handshake: safeString(message.handshake, 80) || descriptor.handshake
206
+ };
207
+ }
208
+
209
+ function normalizeIncomingFrameModeProfile(value) {
210
+ if (!value) {
211
+ return null;
212
+ }
213
+
214
+ if (typeof value === 'string') {
215
+ return getRemoteFrameModeProfile(value);
216
+ }
217
+
218
+ if (typeof value !== 'object') {
219
+ return null;
220
+ }
221
+
222
+ const profile = buildRemoteFrameTransferDescriptor(value.mode || value.frameMode || value.profile);
223
+ return {
224
+ mode: profile.mode,
225
+ label: safeString(value.label, 80) || profile.mode,
226
+ transport: safeString(value.transport, 40) || 'ws-binary',
227
+ encoding: safeString(value.encoding, 40) || profile.encoding,
228
+ codec: safeString(value.codec, 40) || profile.codec,
229
+ compression: safeString(value.compression, 80) || profile.compression,
230
+ profile: safeString(value.profile || value.frameProfile, 80) || profile.frameProfile,
231
+ modeVersion: Number.isFinite(Number(value.modeVersion)) ? Math.max(0, Math.floor(Number(value.modeVersion))) : 1,
232
+ implemented: value.implemented !== false
233
+ };
234
+ }
235
+
61
236
  function isLoopbackHost(value) {
62
237
  const host = String(value || '').trim().toLowerCase();
63
238
  return host === 'localhost'
@@ -542,6 +717,12 @@ function serializeDevice(device, options = {}) {
542
717
  arch: device.arch,
543
718
  pid: device.pid,
544
719
  agentVersion: device.agentVersion,
720
+ protocol: device.protocol,
721
+ protocolVersion: device.protocolVersion,
722
+ frameProtocol: device.frameProtocol ? { ...device.frameProtocol } : null,
723
+ frameModes: Array.isArray(device.frameModes)
724
+ ? device.frameModes.map(profile => ({ ...profile }))
725
+ : [],
545
726
  capabilities: { ...device.capabilities },
546
727
  connected: device.connected,
547
728
  connectedAt: device.connectedAt,
@@ -1086,6 +1267,9 @@ export function createRemoteHub(options = {}) {
1086
1267
  port: boundPort || requestedPort,
1087
1268
  protocol: 'tcp-jsonl',
1088
1269
  protocolVersion: REMOTE_PROTOCOL_VERSION,
1270
+ agentProtocol: REMOTE_AGENT_PROTOCOL,
1271
+ frameProtocol: buildRemoteFrameProtocolDescriptor(),
1272
+ frameModes: getSupportedRemoteFrameModeProfiles(),
1089
1273
  heartbeatMs,
1090
1274
  taskTimeoutMs,
1091
1275
  managerPackage,
@@ -1183,6 +1367,7 @@ export function createRemoteHub(options = {}) {
1183
1367
 
1184
1368
  function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
1185
1369
  const now = new Date().toISOString();
1370
+ const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
1186
1371
  const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
1187
1372
  + Number(device?.counters?.liveFramesReceived || 0)
1188
1373
  + 1;
@@ -1195,6 +1380,14 @@ export function createRemoteHub(options = {}) {
1195
1380
  mimeType: 'image/png',
1196
1381
  format: 'image/png',
1197
1382
  mode,
1383
+ frameMode: descriptor.frameMode,
1384
+ frameProfile: descriptor.frameProfile,
1385
+ encoding: descriptor.encoding,
1386
+ codec: 'png',
1387
+ compression: 'image/png',
1388
+ transferProtocol: descriptor.transferProtocol,
1389
+ transferProtocolVersion: descriptor.transferProtocolVersion,
1390
+ handshake: descriptor.handshake,
1198
1391
  fps: clampNumber(options.fps, 1, 24, mode === 'remote-fast' ? 20 : 1),
1199
1392
  capturedAt: now,
1200
1393
  receivedAt: now,
@@ -1239,7 +1432,7 @@ export function createRemoteHub(options = {}) {
1239
1432
  return null;
1240
1433
  }
1241
1434
 
1242
- const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, 'remote-fast', {
1435
+ const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE, {
1243
1436
  commandId: options.commandId || device.activeLiveStream.commandId,
1244
1437
  width: options.maxWidth || 960,
1245
1438
  height: options.maxHeight || 540,
@@ -1482,11 +1675,23 @@ export function createRemoteHub(options = {}) {
1482
1675
  }
1483
1676
 
1484
1677
  if (isLive) {
1678
+ const transfer = buildRemoteFrameTransferDescriptor(DEFAULT_REMOTE_FRAME_MODE);
1485
1679
  device.activeLiveStream = {
1486
1680
  streamId: `synthetic-live-${ordinal}`,
1487
1681
  commandId: `synthetic-live-command-${ordinal}`,
1488
1682
  active: true,
1489
- mode: 'remote-fast',
1683
+ open: true,
1684
+ openedAt: seenAt,
1685
+ openTransport: 'synthetic',
1686
+ mode: transfer.mode,
1687
+ frameMode: transfer.frameMode,
1688
+ frameProfile: transfer.frameProfile,
1689
+ encoding: transfer.encoding,
1690
+ codec: transfer.codec,
1691
+ compression: transfer.compression,
1692
+ transferProtocol: transfer.transferProtocol,
1693
+ transferProtocolVersion: transfer.transferProtocolVersion,
1694
+ handshake: transfer.handshake,
1490
1695
  fps: 20,
1491
1696
  startedAt: seenAt,
1492
1697
  stoppedAt: '',
@@ -1543,6 +1748,24 @@ export function createRemoteHub(options = {}) {
1543
1748
 
1544
1749
  closeExistingDeviceSocket(deviceId, sessionId);
1545
1750
 
1751
+ const frameProtocol = typeof hello.frameProtocol === 'object' && hello.frameProtocol
1752
+ ? { ...hello.frameProtocol }
1753
+ : null;
1754
+ const frameModes = Array.isArray(hello.frameModes)
1755
+ ? hello.frameModes.map(normalizeIncomingFrameModeProfile).filter(Boolean)
1756
+ : [];
1757
+ const capabilities = typeof hello.capabilities === 'object' && hello.capabilities
1758
+ ? { ...hello.capabilities }
1759
+ : {};
1760
+ if (!capabilities.frameProtocol) {
1761
+ capabilities.frameProtocol = frameProtocol || buildRemoteFrameProtocolDescriptor();
1762
+ }
1763
+ if (!capabilities.frameModes) {
1764
+ capabilities.frameModes = frameModes.length > 0
1765
+ ? frameModes.map(profile => ({ ...profile }))
1766
+ : getSupportedRemoteFrameModeProfiles();
1767
+ }
1768
+
1546
1769
  const device = {
1547
1770
  socket,
1548
1771
  deviceId,
@@ -1553,9 +1776,11 @@ export function createRemoteHub(options = {}) {
1553
1776
  arch: safeString(hello.arch, 40),
1554
1777
  pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
1555
1778
  agentVersion: safeString(hello.agentVersion, 40),
1556
- capabilities: typeof hello.capabilities === 'object' && hello.capabilities
1557
- ? { ...hello.capabilities }
1558
- : {},
1779
+ protocol: safeString(hello.protocol, 80) || REMOTE_AGENT_PROTOCOL,
1780
+ protocolVersion: Number.isFinite(Number(hello.protocolVersion)) ? Math.max(1, Math.floor(Number(hello.protocolVersion))) : 1,
1781
+ frameProtocol: frameProtocol || buildRemoteFrameProtocolDescriptor(),
1782
+ frameModes: frameModes.length > 0 ? frameModes : getSupportedRemoteFrameModeProfiles(),
1783
+ capabilities,
1559
1784
  connected: true,
1560
1785
  connectedAt: now,
1561
1786
  disconnectedAt: '',
@@ -1584,10 +1809,13 @@ export function createRemoteHub(options = {}) {
1584
1809
  sockets.set(socket, deviceId);
1585
1810
  writeJsonLine(socket, {
1586
1811
  type: 'welcome',
1812
+ protocol: REMOTE_AGENT_PROTOCOL,
1587
1813
  protocolVersion: REMOTE_PROTOCOL_VERSION,
1588
1814
  sessionId,
1589
1815
  deviceId,
1590
1816
  heartbeatMs,
1817
+ frameProtocol: buildRemoteFrameProtocolDescriptor(),
1818
+ frameModes: getSupportedRemoteFrameModeProfiles(),
1591
1819
  serverTime: now
1592
1820
  });
1593
1821
 
@@ -2023,6 +2251,7 @@ export function createRemoteHub(options = {}) {
2023
2251
  const payload = buildFramePayloadBuffer(framePayload, frameData);
2024
2252
  const contentHash = buildFrameContentHash(message, payload);
2025
2253
  const sameContentStreak = computeSameContentStreak(device.latestThumbnail, contentHash);
2254
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, 'thumbnail');
2026
2255
  device.latestThumbnail = {
2027
2256
  streamId: safeString(message.streamId, 128) || 'thumbnail',
2028
2257
  frameSeq,
@@ -2031,6 +2260,14 @@ export function createRemoteHub(options = {}) {
2031
2260
  height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
2032
2261
  mimeType,
2033
2262
  format: mimeType,
2263
+ frameMode: transfer.frameMode,
2264
+ frameProfile: transfer.frameProfile,
2265
+ encoding: transfer.encoding,
2266
+ codec: transfer.codec,
2267
+ compression: transfer.compression || mimeType,
2268
+ transferProtocol: transfer.transferProtocol,
2269
+ transferProtocolVersion: transfer.transferProtocolVersion,
2270
+ handshake: transfer.handshake,
2034
2271
  capturedAt,
2035
2272
  receivedAt: device.lastSeenAt,
2036
2273
  byteLength,
@@ -2055,6 +2292,49 @@ export function createRemoteHub(options = {}) {
2055
2292
  return true;
2056
2293
  }
2057
2294
 
2295
+ function applyLiveStreamOpen(device, message, transport = 'json') {
2296
+ const streamId = safeString(message.streamId, 128) || 'live';
2297
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
2298
+ emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
2299
+ reason: 'stale-live-stream-open',
2300
+ streamId,
2301
+ transport
2302
+ });
2303
+ return false;
2304
+ }
2305
+
2306
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
2307
+ const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
2308
+ device.activeLiveStream = {
2309
+ ...device.activeLiveStream,
2310
+ open: true,
2311
+ openedAt,
2312
+ openTransport: transport,
2313
+ mode: transfer.mode,
2314
+ frameMode: transfer.frameMode,
2315
+ frameProfile: transfer.frameProfile,
2316
+ encoding: transfer.encoding,
2317
+ codec: transfer.codec,
2318
+ compression: transfer.compression,
2319
+ transferProtocol: transfer.transferProtocol,
2320
+ transferProtocolVersion: transfer.transferProtocolVersion,
2321
+ handshake: transfer.handshake,
2322
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(device.activeLiveStream.width || 0),
2323
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(device.activeLiveStream.height || 0),
2324
+ openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(device.activeLiveStream.openedFrameSeq || 0)
2325
+ };
2326
+ emitRemoteEvent('RemoteLiveStreamOpened', device, {
2327
+ streamId,
2328
+ mode: transfer.mode,
2329
+ frameMode: transfer.frameMode,
2330
+ frameProfile: transfer.frameProfile,
2331
+ encoding: transfer.encoding,
2332
+ compression: transfer.compression,
2333
+ transport
2334
+ });
2335
+ return true;
2336
+ }
2337
+
2058
2338
  function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
2059
2339
  const frameData = Buffer.isBuffer(framePayload)
2060
2340
  ? ''
@@ -2091,6 +2371,21 @@ export function createRemoteHub(options = {}) {
2091
2371
  const payload = buildFramePayloadBuffer(framePayload, frameData);
2092
2372
  const contentHash = buildFrameContentHash(message, payload);
2093
2373
  const sameContentStreak = computeSameContentStreak(device.latestLiveFrame, contentHash);
2374
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
2375
+ if (device.activeLiveStream.open !== true) {
2376
+ device.activeLiveStream.open = true;
2377
+ device.activeLiveStream.openedAt = device.lastSeenAt;
2378
+ device.activeLiveStream.openTransport = `${transport}-implicit`;
2379
+ device.activeLiveStream.mode = transfer.mode;
2380
+ device.activeLiveStream.frameMode = transfer.frameMode;
2381
+ device.activeLiveStream.frameProfile = transfer.frameProfile;
2382
+ device.activeLiveStream.encoding = transfer.encoding;
2383
+ device.activeLiveStream.codec = transfer.codec;
2384
+ device.activeLiveStream.compression = transfer.compression;
2385
+ device.activeLiveStream.transferProtocol = transfer.transferProtocol;
2386
+ device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
2387
+ device.activeLiveStream.handshake = transfer.handshake;
2388
+ }
2094
2389
  device.latestLiveFrame = {
2095
2390
  streamId,
2096
2391
  frameSeq,
@@ -2099,7 +2394,15 @@ export function createRemoteHub(options = {}) {
2099
2394
  height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
2100
2395
  mimeType,
2101
2396
  format: mimeType,
2102
- mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
2397
+ mode: transfer.mode,
2398
+ frameMode: transfer.frameMode,
2399
+ frameProfile: transfer.frameProfile,
2400
+ encoding: transfer.encoding,
2401
+ codec: transfer.codec,
2402
+ compression: transfer.compression || mimeType,
2403
+ transferProtocol: transfer.transferProtocol,
2404
+ transferProtocolVersion: transfer.transferProtocolVersion,
2405
+ handshake: transfer.handshake,
2103
2406
  fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
2104
2407
  capturedAt,
2105
2408
  receivedAt: device.lastSeenAt,
@@ -2123,6 +2426,15 @@ export function createRemoteHub(options = {}) {
2123
2426
  device.activeLiveStream.lastFrameAt = device.lastSeenAt;
2124
2427
  device.activeLiveStream.lastFrameSeq = frameSeq;
2125
2428
  device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
2429
+ device.activeLiveStream.mode = transfer.mode;
2430
+ device.activeLiveStream.frameMode = transfer.frameMode;
2431
+ device.activeLiveStream.frameProfile = transfer.frameProfile;
2432
+ device.activeLiveStream.encoding = transfer.encoding;
2433
+ device.activeLiveStream.codec = transfer.codec;
2434
+ device.activeLiveStream.compression = transfer.compression;
2435
+ device.activeLiveStream.transferProtocol = transfer.transferProtocol;
2436
+ device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
2437
+ device.activeLiveStream.handshake = transfer.handshake;
2126
2438
  device.counters.liveFramesReceived += 1;
2127
2439
  emitRemoteEvent('RemoteFrameReceived', device, {
2128
2440
  streamId,
@@ -2237,6 +2549,10 @@ export function createRemoteHub(options = {}) {
2237
2549
  applyThumbnailFrame(device, message, message.data, 'json-base64');
2238
2550
  break;
2239
2551
  }
2552
+ case 'stream.open': {
2553
+ applyLiveStreamOpen(device, message, 'json');
2554
+ break;
2555
+ }
2240
2556
  case 'stream.frame': {
2241
2557
  applyLiveFrame(device, message, message.data, 'json-base64');
2242
2558
  break;
@@ -2895,12 +3211,24 @@ export function createRemoteHub(options = {}) {
2895
3211
  const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
2896
3212
  const fps = clampNumber(options.fps, 1, 24, 20);
2897
3213
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
3214
+ const transfer = buildRemoteFrameTransferDescriptor(options.mode || DEFAULT_REMOTE_FRAME_MODE);
2898
3215
  device.counters.commandsSent += 1;
2899
3216
  device.activeLiveStream = {
2900
3217
  streamId,
2901
3218
  commandId,
2902
3219
  active: true,
2903
- mode: 'remote-fast',
3220
+ open: true,
3221
+ openedAt: now,
3222
+ openTransport: 'synthetic',
3223
+ mode: transfer.mode,
3224
+ frameMode: transfer.frameMode,
3225
+ frameProfile: transfer.frameProfile,
3226
+ encoding: transfer.encoding,
3227
+ codec: transfer.codec,
3228
+ compression: transfer.compression,
3229
+ transferProtocol: transfer.transferProtocol,
3230
+ transferProtocolVersion: transfer.transferProtocolVersion,
3231
+ handshake: transfer.handshake,
2904
3232
  fps,
2905
3233
  startedAt: now,
2906
3234
  stoppedAt: '',
@@ -2920,9 +3248,10 @@ export function createRemoteHub(options = {}) {
2920
3248
  streamId,
2921
3249
  commandId,
2922
3250
  fps,
3251
+ mode: transfer.mode,
2923
3252
  synthetic: true
2924
3253
  });
2925
- return { ok: true, commandId, streamId, fps, synthetic: true };
3254
+ return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode, synthetic: true };
2926
3255
  }
2927
3256
 
2928
3257
  if (!device?.socket || device.socket.destroyed || !device.connected) {
@@ -2936,12 +3265,21 @@ export function createRemoteHub(options = {}) {
2936
3265
  const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
2937
3266
  const fps = clampNumber(options.fps, 1, 24, 12);
2938
3267
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
3268
+ const transfer = buildRemoteFrameTransferDescriptor(options.mode || DEFAULT_REMOTE_FRAME_MODE);
2939
3269
  const result = sendCommand(deviceId, {
2940
3270
  command: 'stream.start',
2941
3271
  commandId,
2942
3272
  payload: {
2943
3273
  streamId,
2944
- mode: safeString(options.mode, 80) || 'remote-fast',
3274
+ mode: transfer.mode,
3275
+ frameMode: transfer.frameMode,
3276
+ frameProfile: transfer.frameProfile,
3277
+ encoding: transfer.encoding,
3278
+ codec: transfer.codec,
3279
+ compression: transfer.compression,
3280
+ transferProtocol: transfer.transferProtocol,
3281
+ transferProtocolVersion: transfer.transferProtocolVersion,
3282
+ handshake: transfer.handshake,
2945
3283
  fps,
2946
3284
  maxWidth: clampNumber(options.maxWidth, 320, 2560, 960),
2947
3285
  maxHeight: clampNumber(options.maxHeight, 180, 1440, 540),
@@ -2958,7 +3296,18 @@ export function createRemoteHub(options = {}) {
2958
3296
  streamId,
2959
3297
  commandId,
2960
3298
  active: true,
2961
- mode: 'remote-fast',
3299
+ open: false,
3300
+ openedAt: '',
3301
+ openTransport: '',
3302
+ mode: transfer.mode,
3303
+ frameMode: transfer.frameMode,
3304
+ frameProfile: transfer.frameProfile,
3305
+ encoding: transfer.encoding,
3306
+ codec: transfer.codec,
3307
+ compression: transfer.compression,
3308
+ transferProtocol: transfer.transferProtocol,
3309
+ transferProtocolVersion: transfer.transferProtocolVersion,
3310
+ handshake: transfer.handshake,
2962
3311
  fps,
2963
3312
  startedAt: now,
2964
3313
  stoppedAt: '',
@@ -2971,10 +3320,12 @@ export function createRemoteHub(options = {}) {
2971
3320
  emitRemoteEvent('RemoteLiveStreamStarted', device, {
2972
3321
  streamId,
2973
3322
  commandId,
2974
- fps
3323
+ fps,
3324
+ mode: transfer.mode,
3325
+ frameMode: transfer.frameMode
2975
3326
  });
2976
3327
 
2977
- return { ok: true, commandId, streamId, fps };
3328
+ return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode };
2978
3329
  }
2979
3330
 
2980
3331
  function stopLiveStream(deviceId, options = {}) {
@@ -162,6 +162,7 @@ async function connectManagedAgent(clientBridge, managerEndpoint, staleEndpoint)
162
162
  const result = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/connect`, {
163
163
  method: 'POST',
164
164
  body: JSON.stringify({
165
+ manager: managerEndpoint,
165
166
  managerCandidates: [staleEndpoint, managerEndpoint],
166
167
  pairToken: PAIR_TOKEN,
167
168
  leaseId: LEASE_ID,
@@ -216,9 +217,9 @@ async function main() {
216
217
  assert.equal(firstAgent.ready, true, JSON.stringify(firstAgent));
217
218
  assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
218
219
  assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
219
- assert.ok(
220
- firstConnectMs < 4500,
221
- `managed RemoteAgent must race slow endpoint candidates instead of waiting sequentially, got ${firstConnectMs}ms`);
220
+ assert.equal(firstAgent.manager, managerEndpoint, JSON.stringify(firstAgent));
221
+ assert.equal(firstAgent.managerCandidates?.[0], managerEndpoint, JSON.stringify(firstAgent.managerCandidates));
222
+ assert.ok(firstConnectMs < 4500, `managed RemoteAgent selected manager should connect promptly, got ${firstConnectMs}ms`);
222
223
 
223
224
  const connectedDevice = await waitFor(async () => {
224
225
  const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
@@ -248,7 +249,7 @@ async function main() {
248
249
  assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
249
250
  assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
250
251
  assert.equal(secondAgent.managerCandidates?.[0], managerEndpoint, JSON.stringify(secondAgent.managerCandidates));
251
- assert.equal(secondAgent.managerCandidates?.[1], staleEndpoint, JSON.stringify(secondAgent.managerCandidates));
252
+ assert.equal(secondAgent.managerCandidates?.length, 1, JSON.stringify(secondAgent.managerCandidates));
252
253
 
253
254
  const staleReport = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/sync-report`, {
254
255
  method: 'POST',
@@ -88,6 +88,25 @@ try {
88
88
  pid: process.pid,
89
89
  agentVersion: '0.0.0-ws-smoke',
90
90
  runtime: 'node-smoke',
91
+ protocol: 'mindexec.remote.agent',
92
+ protocolVersion: 2,
93
+ frameProtocol: {
94
+ protocol: 'mindexec.remote.frame.v2',
95
+ version: 2,
96
+ handshake: 'smartview-request-open-frame',
97
+ defaultMode: 'remote-fast'
98
+ },
99
+ frameModes: [{
100
+ mode: 'remote-fast',
101
+ label: 'RemoteFast JPEG Binary',
102
+ transport: 'ws-binary',
103
+ encoding: 'image',
104
+ codec: 'jpeg',
105
+ compression: 'image/jpeg',
106
+ profile: 'jpeg-binary-v1',
107
+ modeVersion: 1,
108
+ implemented: true
109
+ }],
91
110
  capabilities: {
92
111
  status: true,
93
112
  thumbnail: true,
@@ -109,13 +128,20 @@ try {
109
128
  }
110
129
  }));
111
130
 
112
- await waitFor(() => received.find(item => item.type === 'welcome'), 5000, 'welcome');
131
+ const welcome = await waitFor(() => received.find(item => item.type === 'welcome'), 5000, 'welcome');
132
+ assert.equal(welcome.protocol, 'mindexec.remote.agent');
133
+ assert.equal(welcome.frameProtocol?.handshake, 'smartview-request-open-frame');
134
+ assert.equal(welcome.frameProtocol?.defaultMode, 'remote-fast');
135
+ assert.ok(welcome.frameModes?.some(item => item.mode === 'remote-fast'));
113
136
  const device = await waitFor(() => {
114
137
  const current = hub.listDevices();
115
138
  return current.length === 1 && current[0].deviceId === DEVICE_ID ? current[0] : null;
116
139
  }, 5000, 'device registration');
117
140
  assert.equal(device.connected, true);
118
141
  assert.equal(device.capabilities.binaryFrames, true);
142
+ assert.equal(device.protocol, 'mindexec.remote.agent');
143
+ assert.equal(device.frameProtocol?.handshake, 'smartview-request-open-frame');
144
+ assert.ok(device.frameModes?.some(item => item.mode === 'remote-fast'));
119
145
 
120
146
  const inputCommand = hub.sendInputControl(DEVICE_ID, { type: 'noop' });
121
147
  assert.equal(inputCommand.ok, true);
@@ -170,10 +196,36 @@ try {
170
196
  quality: 50
171
197
  });
172
198
  assert.equal(liveCommand.ok, true);
173
- await waitFor(
199
+ const liveMessage = await waitFor(
174
200
  () => received.find(item => item.commandId === liveCommand.commandId),
175
201
  5000,
176
202
  'live command');
203
+ assert.equal(liveMessage.command, 'stream.start');
204
+ assert.equal(liveMessage.payload?.handshake, 'smartview-request-open-frame');
205
+ assert.equal(liveMessage.payload?.mode, 'remote-fast');
206
+ assert.equal(liveMessage.payload?.frameMode, 'remote-fast');
207
+ assert.equal(liveMessage.payload?.frameProfile, 'jpeg-binary-v1');
208
+ assert.equal(liveMessage.payload?.compression, 'image/jpeg');
209
+ assert.equal(liveMessage.payload?.transferProtocol, 'mindexec.remote.frame.v2');
210
+ ws.send(JSON.stringify({
211
+ type: 'stream.open',
212
+ commandId: liveCommand.commandId,
213
+ streamId: 'remote-agent-ws-live',
214
+ mode: 'remote-fast',
215
+ frameMode: 'remote-fast',
216
+ frameProfile: 'jpeg-binary-v1',
217
+ encoding: 'image',
218
+ codec: 'png',
219
+ compression: 'image/png',
220
+ transferProtocol: 'mindexec.remote.frame.v2',
221
+ transferProtocolVersion: 2,
222
+ handshake: 'smartview-request-open-frame',
223
+ fps: 10,
224
+ width: 2,
225
+ height: 1,
226
+ openedAt: new Date().toISOString()
227
+ }));
228
+ await waitFor(() => hub.listDevices()[0]?.activeLiveStream?.open === true, 5000, 'live stream open');
177
229
  writeAgentBinaryFrame(ws, {
178
230
  frameKind: 'stream',
179
231
  commandId: liveCommand.commandId,
@@ -182,6 +234,15 @@ try {
182
234
  width: 2,
183
235
  height: 1,
184
236
  mimeType: 'image/png',
237
+ mode: 'remote-fast',
238
+ frameMode: 'remote-fast',
239
+ frameProfile: 'jpeg-binary-v1',
240
+ encoding: 'image',
241
+ codec: 'png',
242
+ compression: 'image/png',
243
+ transferProtocol: 'mindexec.remote.frame.v2',
244
+ transferProtocolVersion: 2,
245
+ handshake: 'smartview-request-open-frame',
185
246
  fps: 10,
186
247
  capturedAt: new Date().toISOString()
187
248
  }, SMOKE_PNG);
@@ -191,6 +252,12 @@ try {
191
252
  }, 5000, 'websocket binary live frame');
192
253
  assert.equal(liveDevice.latestLiveFrame.transport, 'binary');
193
254
  assert.equal(liveDevice.latestLiveFrame.streamId, 'remote-agent-ws-live');
255
+ assert.equal(liveDevice.activeLiveStream.open, true);
256
+ assert.equal(liveDevice.activeLiveStream.frameMode, 'remote-fast');
257
+ assert.equal(liveDevice.latestLiveFrame.frameMode, 'remote-fast');
258
+ assert.equal(liveDevice.latestLiveFrame.frameProfile, 'jpeg-binary-v1');
259
+ assert.equal(liveDevice.latestLiveFrame.compression, 'image/png');
260
+ assert.equal(liveDevice.latestLiveFrame.transferProtocol, 'mindexec.remote.frame.v2');
194
261
 
195
262
  ws.close();
196
263
  console.log('RemoteAgent WebSocket smoke OK');
@@ -214,6 +214,23 @@ try {
214
214
  quality: 50
215
215
  });
216
216
  assert.equal(liveCommand.ok, true);
217
+ assert.equal(liveCommand.frameMode, 'remote-fast');
218
+ writeJsonLine(socket, {
219
+ type: 'stream.open',
220
+ commandId: liveCommand.commandId,
221
+ streamId: 'smoke-live',
222
+ mode: 'remote-fast',
223
+ frameMode: 'remote-fast',
224
+ frameProfile: 'jpeg-binary-v1',
225
+ encoding: 'image',
226
+ codec: 'png',
227
+ compression: 'image/png',
228
+ transferProtocol: 'mindexec.remote.frame.v2',
229
+ transferProtocolVersion: 2,
230
+ handshake: 'smartview-request-open-frame',
231
+ fps: 5,
232
+ openedAt: new Date().toISOString()
233
+ });
217
234
  writeJsonLine(socket, {
218
235
  type: 'stream.frame',
219
236
  commandId: liveCommand.commandId,
@@ -223,6 +240,14 @@ try {
223
240
  height: 1,
224
241
  mimeType: 'image/png',
225
242
  mode: 'remote-fast',
243
+ frameMode: 'remote-fast',
244
+ frameProfile: 'jpeg-binary-v1',
245
+ encoding: 'image',
246
+ codec: 'png',
247
+ compression: 'image/png',
248
+ transferProtocol: 'mindexec.remote.frame.v2',
249
+ transferProtocolVersion: 2,
250
+ handshake: 'smartview-request-open-frame',
226
251
  fps: 5,
227
252
  capturedAt: new Date().toISOString(),
228
253
  data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
@@ -233,7 +258,11 @@ try {
233
258
  return current[0]?.latestLiveFrame?.streamId === 'smoke-live' ? current[0] : null;
234
259
  });
235
260
  assert.equal(liveDevice.activeLiveStream.active, true);
261
+ assert.equal(liveDevice.activeLiveStream.open, true);
262
+ assert.equal(liveDevice.activeLiveStream.handshake, 'smartview-request-open-frame');
236
263
  assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
264
+ assert.equal(liveDevice.latestLiveFrame.frameMode, 'remote-fast');
265
+ assert.equal(liveDevice.latestLiveFrame.compression, 'image/png');
237
266
  assert.equal(liveDevice.counters.liveFramesReceived, 1);
238
267
  const serializedFirstLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
239
268
  const serializedFirstLiveUrl = new URL(serializedFirstLiveFrame.framePath, 'http://127.0.0.1');
@@ -247,6 +276,14 @@ try {
247
276
  height: 1,
248
277
  mimeType: 'image/png',
249
278
  mode: 'remote-fast',
279
+ frameMode: 'remote-fast',
280
+ frameProfile: 'jpeg-binary-v1',
281
+ encoding: 'image',
282
+ codec: 'png',
283
+ compression: 'image/png',
284
+ transferProtocol: 'mindexec.remote.frame.v2',
285
+ transferProtocolVersion: 2,
286
+ handshake: 'smartview-request-open-frame',
250
287
  fps: 5,
251
288
  capturedAt: new Date().toISOString()
252
289
  }, smokePngFrame);
@@ -265,6 +302,8 @@ try {
265
302
  const serializedBinaryLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
266
303
  assert.equal(serializedBinaryLiveFrame.streamId, 'smoke-live');
267
304
  assert.equal(serializedBinaryLiveFrame.frameSeq, 5);
305
+ assert.equal(serializedBinaryLiveFrame.frameMode, 'remote-fast');
306
+ assert.equal(serializedBinaryLiveFrame.compression, 'image/png');
268
307
  assert.equal(serializedBinaryLiveFrame.contentHash, binaryLiveDevice.latestLiveFrame.contentHash);
269
308
  assert.ok(serializedBinaryLiveFrame.framePath.includes('/api/remote/devices/smoke-device/live/frame?'));
270
309
  assert.ok(!('dataUrl' in serializedBinaryLiveFrame));
@@ -584,7 +584,8 @@ async function main() {
584
584
  assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
585
585
  assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
586
586
  assert.equal(firstAgent.manager, hostAEndpoint);
587
- assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
587
+ assert.equal(firstAgent.managerCandidates?.[0], hostAEndpoint, JSON.stringify(firstAgent.managerCandidates));
588
+ assert.equal(firstAgent.managerCandidates?.length, 1, JSON.stringify(firstAgent.managerCandidates));
588
589
  assert.ok(
589
590
  fakeSupabase.requests.some(request =>
590
591
  request.method === 'POST'
package/server.js CHANGED
@@ -2550,12 +2550,14 @@ function clampRemoteFrameWsNumber(value, min, max, fallback) {
2550
2550
  }
2551
2551
 
2552
2552
  function normalizeRemoteFrameWsLiveOptions(payload = {}) {
2553
+ const mode = String(payload.frameMode || payload.mode || 'remote-fast').trim() || 'remote-fast';
2553
2554
  return {
2554
2555
  fps: clampRemoteFrameWsNumber(payload.fps, 1, 24, REMOTE_FRAME_WS_DEFAULT_FPS),
2555
2556
  maxWidth: clampRemoteFrameWsNumber(payload.maxWidth, 320, 2560, REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH),
2556
2557
  maxHeight: clampRemoteFrameWsNumber(payload.maxHeight, 180, 1440, REMOTE_FRAME_WS_DEFAULT_MAX_HEIGHT),
2557
2558
  quality: clampRemoteFrameWsNumber(payload.quality, 20, 95, REMOTE_FRAME_WS_DEFAULT_QUALITY),
2558
- mode: String(payload.mode || 'remote-fast').trim() || 'remote-fast'
2559
+ mode,
2560
+ frameMode: mode
2559
2561
  };
2560
2562
  }
2561
2563
 
@@ -2671,6 +2673,14 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
2671
2673
  height: Number(frame.height || 0) || 0,
2672
2674
  mimeType: frameEvent.mimeType || frame.mimeType || frame.format || 'image/jpeg',
2673
2675
  mode: frame.mode || '',
2676
+ frameMode: frame.frameMode || frame.mode || '',
2677
+ frameProfile: frame.frameProfile || '',
2678
+ encoding: frame.encoding || '',
2679
+ codec: frame.codec || '',
2680
+ compression: frame.compression || frame.mimeType || frame.format || '',
2681
+ transferProtocol: frame.transferProtocol || '',
2682
+ transferProtocolVersion: Number(frame.transferProtocolVersion || 0) || 0,
2683
+ handshake: frame.handshake || '',
2674
2684
  fps: Number(frame.fps || 0) || 0,
2675
2685
  capturedAt: frame.capturedAt || '',
2676
2686
  receivedAt: frame.receivedAt || '',
@@ -3625,7 +3635,9 @@ const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
3625
3635
  const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
3626
3636
  const REMOTE_AGENT_SYNC_REPORT_WAKE_MS = Math.max(500, Number(process.env.MINDEXEC_REMOTE_AGENT_SYNC_REPORT_WAKE_MS || 1500) || 1500);
3627
3637
  const REMOTE_AGENT_RACE_START_STAGGER_MS = Math.max(0, Number(process.env.MINDEXEC_REMOTE_AGENT_RACE_STAGGER_MS ?? 0) || 0);
3628
- const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
3638
+ const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = Math.max(
3639
+ 1,
3640
+ Math.min(6, Number(process.env.MINDEXEC_REMOTE_AGENT_MAX_PARALLEL_CANDIDATES || 1) || 1));
3629
3641
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
3630
3642
  const REMOTE_AGENT_RECENT_MANAGER_CACHE_LIMIT = 64;
3631
3643
  const REMOTE_AGENT_RECENT_MANAGER_DEFAULT_KEY = 'default';
@@ -7397,7 +7409,7 @@ async function runRemoteRegistryFollowerOnce(trigger = 'timer') {
7397
7409
  }
7398
7410
 
7399
7411
  const connect = await startRemoteAgentConnection({
7400
- manager: target.endpointCandidates[0],
7412
+ manager: target.endpoint,
7401
7413
  managerCandidates: target.endpointCandidates,
7402
7414
  pairToken: target.pairToken,
7403
7415
  leaseId: target.leaseId,