@mindexec/cli 0.2.119 → 0.2.121

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.119",
3
+ "version": "0.2.121",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -372,8 +372,8 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
372
372
  framePath,
373
373
  frameUrl: framePath
374
374
  };
375
- if (options.includeDataUrl !== false) {
376
- serialized.dataUrl = dataUrl;
375
+ if (options.includeDataUrl === true) {
376
+ serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
377
377
  }
378
378
 
379
379
  return serialized;
@@ -1114,7 +1114,7 @@ export function createRemoteHub(options = {}) {
1114
1114
 
1115
1115
  function listDevices(options = {}) {
1116
1116
  const serializeOptions = {
1117
- includeDataUrl: options.includeDataUrl !== false
1117
+ includeDataUrl: options.includeDataUrl === true
1118
1118
  };
1119
1119
  return [...devices.values()]
1120
1120
  .map(device => serializeDevice(device, serializeOptions))
@@ -1969,7 +1969,6 @@ export function createRemoteHub(options = {}) {
1969
1969
  contentHash,
1970
1970
  captureMs: readFrameCaptureMs(message),
1971
1971
  sameContentStreak,
1972
- dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
1973
1972
  payload,
1974
1973
  accessToken: createFrameAccessToken()
1975
1974
  };
@@ -2040,7 +2039,6 @@ export function createRemoteHub(options = {}) {
2040
2039
  contentHash,
2041
2040
  captureMs: readFrameCaptureMs(message),
2042
2041
  sameContentStreak,
2043
- dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
2044
2042
  payload,
2045
2043
  accessToken: createFrameAccessToken()
2046
2044
  };
@@ -2961,14 +2959,14 @@ export function createRemoteHub(options = {}) {
2961
2959
  function getDeviceLiveFrame(deviceId, options = {}) {
2962
2960
  const device = devices.get(String(deviceId || ''));
2963
2961
  return serializeRemoteFrame(device?.latestLiveFrame, device?.deviceId, 'live', {
2964
- includeDataUrl: options.includeDataUrl !== false
2962
+ includeDataUrl: options.includeDataUrl === true
2965
2963
  });
2966
2964
  }
2967
2965
 
2968
2966
  function getDeviceThumbnail(deviceId, options = {}) {
2969
2967
  const device = devices.get(String(deviceId || ''));
2970
2968
  return serializeRemoteFrame(device?.latestThumbnail, device?.deviceId, 'thumbnail', {
2971
- includeDataUrl: options.includeDataUrl !== false
2969
+ includeDataUrl: options.includeDataUrl === true
2972
2970
  });
2973
2971
  }
2974
2972
 
@@ -373,10 +373,16 @@ async function runSyntheticEnabledSmoke() {
373
373
  });
374
374
  assert.equal(thumbnail.ok, true, JSON.stringify(thumbnail.payload));
375
375
  assert.equal(thumbnail.payload?.thumbnail?.streamId, 'http-smoke-thumb');
376
- assert.ok(String(thumbnail.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
376
+ assert.ok(!('dataUrl' in thumbnail.payload.thumbnail));
377
377
  assert.ok(String(thumbnail.payload?.thumbnail?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail?`));
378
378
  assert.equal(thumbnail.payload?.thumbnail?.frameUrl, thumbnail.payload?.thumbnail?.framePath);
379
379
 
380
+ const thumbnailWithData = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail?includeDataUrl=true`, {
381
+ token: BRIDGE_TOKEN
382
+ });
383
+ assert.equal(thumbnailWithData.ok, true, JSON.stringify(thumbnailWithData.payload));
384
+ assert.ok(String(thumbnailWithData.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
385
+
380
386
  const thumbnailBinary = await fetchBinary(`${baseUrl}${thumbnail.payload.thumbnail.framePath}`);
381
387
  assert.equal(thumbnailBinary.ok, true, `${thumbnailBinary.status} ${thumbnailBinary.contentType}`);
382
388
  assert.equal(thumbnailBinary.contentType, 'image/png');
@@ -433,9 +439,15 @@ async function runSyntheticEnabledSmoke() {
433
439
  assert.equal(liveFrame.payload?.frame?.streamId, 'http-smoke-live');
434
440
  assert.equal(liveFrame.payload?.frame?.mode, 'remote-fast');
435
441
  assert.equal(liveFrame.payload?.frame?.fps, 20);
436
- assert.ok(String(liveFrame.payload?.frame?.dataUrl || '').startsWith('data:image/png;base64,'));
442
+ assert.ok(!('dataUrl' in liveFrame.payload.frame));
437
443
  assert.ok(String(liveFrame.payload?.frame?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?`));
438
444
 
445
+ const liveFrameWithData = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?includeDataUrl=true`, {
446
+ token: BRIDGE_TOKEN
447
+ });
448
+ assert.equal(liveFrameWithData.ok, true, JSON.stringify(liveFrameWithData.payload));
449
+ assert.ok(String(liveFrameWithData.payload?.frame?.dataUrl || '').startsWith('data:image/png;base64,'));
450
+
439
451
  const liveBinary = await fetchBinary(`${baseUrl}${liveFrame.payload.frame.framePath}`);
440
452
  assert.equal(liveBinary.ok, true, `${liveBinary.status} ${liveBinary.contentType}`);
441
453
  assert.equal(liveBinary.contentType, 'image/png');
@@ -37,9 +37,12 @@ try {
37
37
  assert.equal(devices.length, SYNTHETIC_COUNT);
38
38
  assert.equal(devices.filter(device => device.synthetic === true).length, SYNTHETIC_COUNT);
39
39
  assert.equal(devices.some(device => device.connected === false), true);
40
- assert.equal(devices.some(device => device.latestThumbnail?.dataUrl), true);
40
+ assert.equal(devices.some(device => device.latestThumbnail && 'dataUrl' in device.latestThumbnail), false);
41
41
  assert.equal(devices.some(device => device.activeLiveStream?.active === true), true);
42
42
 
43
+ const devicesWithDataUrl = hub.listDevices({ includeDataUrl: true });
44
+ assert.equal(devicesWithDataUrl.some(device => device.latestThumbnail?.dataUrl), true);
45
+
43
46
  const deviceListPayload = {
44
47
  total: devices.length,
45
48
  pagination: 'none',
@@ -58,6 +61,8 @@ try {
58
61
  });
59
62
  assert.equal(thumbnailResult.ok, true);
60
63
  assert.equal(hub.getDeviceThumbnail(thumbnailTarget.deviceId).streamId, 'scale-thumb');
64
+ assert.equal('dataUrl' in hub.getDeviceThumbnail(thumbnailTarget.deviceId), false);
65
+ assert.ok(hub.getDeviceThumbnail(thumbnailTarget.deviceId, { includeDataUrl: true }).dataUrl);
61
66
 
62
67
  const liveTarget = devices.find(device => device.connected && device.capabilities?.liveStream);
63
68
  assert.ok(liveTarget);
package/server.js CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  import express from 'express';
10
10
  import cors from 'cors';
11
- import { promises as fs, readFileSync, statSync, createReadStream } from 'fs';
11
+ import { promises as fs, readFileSync, statSync, createReadStream, chmodSync } from 'fs';
12
12
  import path from 'path';
13
- import { exec, spawn, execFile } from 'child_process';
13
+ import { exec, spawn, spawnSync, execFile } from 'child_process';
14
14
  import { promisify } from 'util';
15
15
  import os from 'os';
16
16
  import multer from 'multer';
@@ -2907,10 +2907,14 @@ const REMOTE_AGENT_EARLY_EXIT_MS = 1200;
2907
2907
  const REMOTE_AGENT_READY_TIMEOUT_MS = 5000;
2908
2908
  const REMOTE_AGENT_DEFAULT_ENGINE = 'auto';
2909
2909
  const REMOTE_AGENT_SYNC_REPORT_LOG_REPEAT_MS = 30000;
2910
+ const REMOTE_AGENT_RACE_START_STAGGER_MS = 120;
2911
+ const REMOTE_AGENT_MAX_PARALLEL_CANDIDATES = 6;
2910
2912
  let remoteAgentState = createRemoteAgentIdleState();
2911
2913
  let remoteAgentSyncReportState = null;
2912
2914
  let remoteAgentSyncReportLogKey = '';
2913
2915
  let remoteAgentSyncReportLogAt = 0;
2916
+ let remoteAgentConnectPromise = null;
2917
+ const remoteAgentRecentSuccessfulManagers = new Map();
2914
2918
 
2915
2919
  function createRemoteAgentIdleState(overrides = {}) {
2916
2920
  return {
@@ -3211,6 +3215,46 @@ function createRemoteAgentConnectionKey(manager, leaseId) {
3211
3215
  .slice(0, 24);
3212
3216
  }
3213
3217
 
3218
+ function getRemoteAgentRecentManagerKey(leaseId = '') {
3219
+ return safeRemoteAgentField(leaseId || 'default', 128) || 'default';
3220
+ }
3221
+
3222
+ function rememberRemoteAgentSuccessfulManager(manager, leaseId = '') {
3223
+ const endpoint = normalizeRemoteManagerEndpoint(manager);
3224
+ if (!endpoint) {
3225
+ return;
3226
+ }
3227
+
3228
+ remoteAgentRecentSuccessfulManagers.set(getRemoteAgentRecentManagerKey(leaseId), {
3229
+ endpoint,
3230
+ updatedAt: Date.now()
3231
+ });
3232
+ }
3233
+
3234
+ function prioritizeRemoteAgentManagers(managers, leaseId = '') {
3235
+ const normalized = normalizeRemoteManagerEndpointList(managers);
3236
+ if (normalized.length <= 1) {
3237
+ return normalized;
3238
+ }
3239
+
3240
+ const recent = remoteAgentRecentSuccessfulManagers.get(getRemoteAgentRecentManagerKey(leaseId));
3241
+ const recentEndpoint = normalizeRemoteManagerEndpoint(recent?.endpoint);
3242
+ if (!recentEndpoint) {
3243
+ return normalized;
3244
+ }
3245
+
3246
+ const recentIndex = normalized.findIndex(item => item.toLowerCase() === recentEndpoint.toLowerCase());
3247
+ if (recentIndex <= 0) {
3248
+ return normalized;
3249
+ }
3250
+
3251
+ return [
3252
+ normalized[recentIndex],
3253
+ ...normalized.slice(0, recentIndex),
3254
+ ...normalized.slice(recentIndex + 1)
3255
+ ];
3256
+ }
3257
+
3214
3258
  function isRemoteAgentProcessRunning() {
3215
3259
  const proc = remoteAgentState.proc;
3216
3260
  return remoteAgentState.status === 'running'
@@ -3225,19 +3269,183 @@ function isLocalRemoteHostTargetActive() {
3225
3269
  && (!status.hostTargetHostInstanceId || status.hostTargetHostInstanceId === BRIDGE_INSTANCE_ID);
3226
3270
  }
3227
3271
 
3228
- function resolveRemoteAgentLauncher() {
3229
- const localLauncher = path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'bin', 'mindexec-remote.js');
3230
- try {
3231
- if (statSync(localLauncher).isFile()) {
3272
+ function getRemoteFastRuntimeId() {
3273
+ const platform = process.platform;
3274
+ const arch = process.arch;
3275
+ if (platform === 'win32' && arch === 'x64') {
3276
+ return 'win-x64';
3277
+ }
3278
+ if (platform === 'darwin' && arch === 'arm64') {
3279
+ return 'osx-arm64';
3280
+ }
3281
+ if (platform === 'darwin' && arch === 'x64') {
3282
+ return 'osx-x64';
3283
+ }
3284
+ return '';
3285
+ }
3286
+
3287
+ function getRemoteFastRuntimeExecutableName() {
3288
+ return process.platform === 'win32'
3289
+ ? 'mindexec-remote-fast.exe'
3290
+ : 'mindexec-remote-fast';
3291
+ }
3292
+
3293
+ function getBundledRemoteFastRuntimeDirs() {
3294
+ const rid = getRemoteFastRuntimeId();
3295
+ if (!rid) {
3296
+ return [];
3297
+ }
3298
+
3299
+ return [
3300
+ path.join(BRIDGE_ROOT, 'remote-fast', rid),
3301
+ path.join(BRIDGE_ROOT, 'remote', 'fast', rid),
3302
+ path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'fast', rid),
3303
+ path.resolve(BRIDGE_ROOT, '..', 'remote', 'fast', rid),
3304
+ path.resolve(BRIDGE_ROOT, '..', 'RemoteAgent', 'fast', rid)
3305
+ ];
3306
+ }
3307
+
3308
+ function resolveRemoteFastLauncher() {
3309
+ const rid = getRemoteFastRuntimeId();
3310
+ if (!rid) {
3311
+ return {
3312
+ ok: false,
3313
+ error: `remote-fast-not-packaged:${process.platform}-${process.arch}`
3314
+ };
3315
+ }
3316
+
3317
+ const failures = [];
3318
+ const seen = new Set();
3319
+ for (const dir of getBundledRemoteFastRuntimeDirs()) {
3320
+ const resolvedDir = path.resolve(dir);
3321
+ const key = resolvedDir.toLowerCase();
3322
+ if (seen.has(key)) {
3323
+ continue;
3324
+ }
3325
+ seen.add(key);
3326
+
3327
+ const executable = path.join(resolvedDir, getRemoteFastRuntimeExecutableName());
3328
+ try {
3329
+ if (statSync(executable).isFile()) {
3330
+ if (process.platform !== 'win32') {
3331
+ try {
3332
+ const currentMode = statSync(executable).mode;
3333
+ chmodSync(executable, currentMode | 0o755);
3334
+ } catch {
3335
+ // best effort only
3336
+ }
3337
+ }
3338
+
3339
+ const preflight = spawnSync(executable, ['--version'], {
3340
+ encoding: 'utf8',
3341
+ windowsHide: true,
3342
+ timeout: 2500
3343
+ });
3344
+ if (preflight.status === 0) {
3345
+ return {
3346
+ ok: true,
3347
+ command: executable,
3348
+ argsPrefix: [],
3349
+ launcher: executable,
3350
+ usingNpx: false,
3351
+ directFast: true,
3352
+ rid
3353
+ };
3354
+ }
3355
+ failures.push(`${executable}:exit:${preflight.status}`);
3356
+ }
3357
+ } catch {
3358
+ // Try DLL or next directory.
3359
+ }
3360
+
3361
+ const dll = path.join(resolvedDir, 'mindexec-remote-fast.dll');
3362
+ try {
3363
+ if (statSync(dll).isFile()) {
3364
+ const preflight = spawnSync('dotnet', [dll, '--version'], {
3365
+ encoding: 'utf8',
3366
+ windowsHide: true,
3367
+ timeout: 2500
3368
+ });
3369
+ if (preflight.status === 0) {
3370
+ return {
3371
+ ok: true,
3372
+ command: 'dotnet',
3373
+ argsPrefix: [dll],
3374
+ launcher: dll,
3375
+ usingNpx: false,
3376
+ directFast: true,
3377
+ rid
3378
+ };
3379
+ }
3380
+ failures.push(`${dll}:exit:${preflight.status}`);
3381
+ }
3382
+ } catch {
3383
+ // Try next directory.
3384
+ }
3385
+ }
3386
+
3387
+ return {
3388
+ ok: false,
3389
+ error: failures.length > 0
3390
+ ? failures.join('; ')
3391
+ : `remote-fast-runtime-missing:${rid}`
3392
+ };
3393
+ }
3394
+
3395
+ function resolveLocalRemotePackageLaunchers() {
3396
+ return [
3397
+ path.join(BRIDGE_ROOT, 'remote', 'bin', 'mindexec-remote.js'),
3398
+ path.join(BRIDGE_ROOT, 'node_modules', '@mindexec', 'remote', 'bin', 'mindexec-remote.js'),
3399
+ path.resolve(BRIDGE_ROOT, '..', 'remote', 'bin', 'mindexec-remote.js'),
3400
+ path.resolve(BRIDGE_ROOT, '..', 'RemoteAgent', 'bin', 'mindexec-remote.js')
3401
+ ];
3402
+ }
3403
+
3404
+ function resolveRemotePackageLauncher() {
3405
+ const seen = new Set();
3406
+ for (const candidate of resolveLocalRemotePackageLaunchers()) {
3407
+ const resolved = path.resolve(candidate);
3408
+ const key = resolved.toLowerCase();
3409
+ if (seen.has(key)) {
3410
+ continue;
3411
+ }
3412
+ seen.add(key);
3413
+ try {
3414
+ if (statSync(resolved).isFile()) {
3415
+ return {
3416
+ command: process.execPath,
3417
+ argsPrefix: [resolved],
3418
+ launcher: resolved,
3419
+ usingNpx: false,
3420
+ directFast: false
3421
+ };
3422
+ }
3423
+ } catch {
3424
+ // Try next package layout.
3425
+ }
3426
+ }
3427
+ return null;
3428
+ }
3429
+
3430
+ function resolveRemoteAgentLauncher(engine = REMOTE_AGENT_DEFAULT_ENGINE) {
3431
+ const normalizedEngine = normalizeRemoteAgentEngine(engine);
3432
+ if (normalizedEngine !== 'node') {
3433
+ const fast = resolveRemoteFastLauncher();
3434
+ if (fast.ok) {
3435
+ return fast;
3436
+ }
3437
+
3438
+ if (normalizedEngine === 'fast') {
3232
3439
  return {
3233
- command: process.execPath,
3234
- argsPrefix: [localLauncher],
3235
- launcher: localLauncher,
3236
- usingNpx: false
3440
+ ok: false,
3441
+ error: fast.error || 'remote-fast-unavailable'
3237
3442
  };
3238
3443
  }
3239
- } catch {
3240
- // Fall back to npx for source checkouts or older package installs.
3444
+ }
3445
+
3446
+ const localPackage = resolveRemotePackageLauncher();
3447
+ if (localPackage) {
3448
+ return localPackage;
3241
3449
  }
3242
3450
 
3243
3451
  const npxCli = resolveNpxCliLauncher();
@@ -3246,7 +3454,8 @@ function resolveRemoteAgentLauncher() {
3246
3454
  command: process.execPath,
3247
3455
  argsPrefix: [npxCli, '-y', '@mindexec/remote@latest'],
3248
3456
  launcher: npxCli,
3249
- usingNpx: true
3457
+ usingNpx: true,
3458
+ directFast: false
3250
3459
  };
3251
3460
  }
3252
3461
 
@@ -3255,7 +3464,8 @@ function resolveRemoteAgentLauncher() {
3255
3464
  command: process.env.ComSpec || 'cmd.exe',
3256
3465
  argsPrefix: ['/d', '/s', '/c', 'npx.cmd', '-y', '@mindexec/remote@latest'],
3257
3466
  launcher: 'cmd.exe /c npx.cmd',
3258
- usingNpx: true
3467
+ usingNpx: true,
3468
+ directFast: false
3259
3469
  };
3260
3470
  }
3261
3471
 
@@ -3263,7 +3473,8 @@ function resolveRemoteAgentLauncher() {
3263
3473
  command: 'npx',
3264
3474
  argsPrefix: ['-y', '@mindexec/remote@latest'],
3265
3475
  launcher: 'npx',
3266
- usingNpx: true
3476
+ usingNpx: true,
3477
+ directFast: false
3267
3478
  };
3268
3479
  }
3269
3480
 
@@ -3343,11 +3554,11 @@ async function startRemoteAgentConnection(options = {}) {
3343
3554
  const nodeId = safeRemoteAgentField(options.nodeId, 128);
3344
3555
  const source = safeRemoteAgentField(options.source || 'registry', 64);
3345
3556
  const engine = normalizeRemoteAgentEngine(options.engine);
3346
- const managers = normalizeRemoteManagerEndpointList(
3557
+ const managers = prioritizeRemoteAgentManagers(normalizeRemoteManagerEndpointList(
3347
3558
  options.manager || options.managerEndpoint || options.endpoint,
3348
3559
  options.managerCandidates,
3349
3560
  options.endpointCandidates,
3350
- options.candidates);
3561
+ options.candidates), leaseId);
3351
3562
 
3352
3563
  if (managers.length === 0) {
3353
3564
  logWarn('remote', 'managed RemoteAgent connect rejected: invalid manager endpoint');
@@ -3385,45 +3596,321 @@ async function startRemoteAgentConnection(options = {}) {
3385
3596
  await stopRemoteAgentConnection('replaced-by-new-target');
3386
3597
  }
3387
3598
 
3388
- let lastResult = null;
3599
+ if (remoteAgentConnectPromise) {
3600
+ return await remoteAgentConnectPromise;
3601
+ }
3602
+
3603
+ remoteAgentConnectPromise = startRemoteAgentConnectionRace({
3604
+ managers,
3605
+ pairToken,
3606
+ leaseId,
3607
+ nodeId,
3608
+ source,
3609
+ engine
3610
+ }).finally(() => {
3611
+ remoteAgentConnectPromise = null;
3612
+ });
3613
+ return await remoteAgentConnectPromise;
3614
+ }
3615
+
3616
+ function buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine) {
3617
+ const args = [
3618
+ ...launcher.argsPrefix,
3619
+ 'connect',
3620
+ '--manager',
3621
+ manager,
3622
+ '--pair',
3623
+ pairToken
3624
+ ];
3625
+
3626
+ if (launcher.directFast === true) {
3627
+ args.push('--transport', 'ws');
3628
+ return args;
3629
+ }
3630
+
3631
+ args.push('--engine', engine);
3632
+ return args;
3633
+ }
3634
+
3635
+ function appendRemoteAgentAttemptOutput(attempt, stream, chunk) {
3636
+ const text = chunk.toString();
3637
+ const key = stream === 'stderr' ? 'stderrTail' : 'stdoutTail';
3638
+ attempt.state[key] = String((attempt.state[key] || '') + text).slice(-REMOTE_AGENT_STDIO_TAIL_CHARS);
3639
+ if (/Connected to RemoteHub/i.test(text)) {
3640
+ attempt.state.ready = true;
3641
+ attempt.state.connectedAt = new Date().toISOString();
3642
+ }
3643
+ attempt.state.updatedAt = new Date().toISOString();
3644
+ }
3645
+
3646
+ function summarizeRemoteAgentAttempt(attempt) {
3647
+ return formatRemoteAgentFailureSummary(attempt?.state || {});
3648
+ }
3649
+
3650
+ function createRemoteAgentAttempt(options = {}) {
3651
+ const manager = normalizeRemoteManagerEndpoint(options.manager);
3652
+ const pairToken = safeRemoteAgentField(options.pairToken || options.pair, 512);
3653
+ const leaseId = safeRemoteAgentField(options.leaseId, 128);
3654
+ const nodeId = safeRemoteAgentField(options.nodeId, 128);
3655
+ const source = safeRemoteAgentField(options.source || 'registry', 64);
3656
+ const engine = normalizeRemoteAgentEngine(options.engine);
3657
+ const managerCandidates = normalizeRemoteManagerEndpointList(options.managerCandidates, manager);
3658
+
3659
+ const connectionKey = createRemoteAgentConnectionKey(manager, leaseId);
3660
+ const launcher = options.launcher || resolveRemoteAgentLauncher(engine);
3661
+ if (launcher?.ok === false) {
3662
+ return {
3663
+ manager,
3664
+ failed: true,
3665
+ error: launcher.error || 'remote-agent-launcher-unavailable',
3666
+ state: createRemoteAgentIdleState({
3667
+ status: 'failed',
3668
+ manager,
3669
+ managerCandidates,
3670
+ leaseId,
3671
+ nodeId,
3672
+ engine,
3673
+ source,
3674
+ connectionKey,
3675
+ lastError: launcher.error || 'remote-agent-launcher-unavailable'
3676
+ })
3677
+ };
3678
+ }
3679
+
3680
+ const state = createRemoteAgentIdleState({
3681
+ status: 'starting',
3682
+ manager,
3683
+ managerCandidates,
3684
+ leaseId,
3685
+ nodeId,
3686
+ engine,
3687
+ source,
3688
+ connectionKey,
3689
+ startedAt: new Date().toISOString(),
3690
+ ready: false,
3691
+ connectedAt: '',
3692
+ launcher: launcher.launcher,
3693
+ usingNpx: launcher.usingNpx
3694
+ });
3695
+
3696
+ const args = buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine);
3697
+ const attempt = {
3698
+ manager,
3699
+ pairToken,
3700
+ leaseId,
3701
+ nodeId,
3702
+ source,
3703
+ engine,
3704
+ launcher,
3705
+ args,
3706
+ state,
3707
+ child: null,
3708
+ failed: false,
3709
+ error: ''
3710
+ };
3711
+
3712
+ try {
3713
+ const child = spawn(launcher.command, args, {
3714
+ cwd: BRIDGE_ROOT,
3715
+ stdio: ['ignore', 'pipe', 'pipe'],
3716
+ windowsHide: true,
3717
+ env: {
3718
+ ...process.env,
3719
+ MINDEXEC_REMOTE_AGENT_MANAGED: '1'
3720
+ }
3721
+ });
3722
+
3723
+ attempt.child = child;
3724
+ state.proc = child;
3725
+ state.pid = Number(child.pid || 0);
3726
+ state.status = 'running';
3727
+ state.updatedAt = new Date().toISOString();
3728
+
3729
+ child.stdout.on('data', chunk => appendRemoteAgentAttemptOutput(attempt, 'stdout', chunk));
3730
+ child.stderr.on('data', chunk => appendRemoteAgentAttemptOutput(attempt, 'stderr', chunk));
3731
+ child.once('error', err => {
3732
+ attempt.failed = true;
3733
+ attempt.error = err?.message || String(err);
3734
+ state.status = 'failed';
3735
+ state.lastError = attempt.error;
3736
+ state.updatedAt = new Date().toISOString();
3737
+ });
3738
+ child.once('exit', (code, signal) => {
3739
+ if (state.status === 'running' && state.ready === true) {
3740
+ state.status = code === 0 ? 'exited' : 'failed';
3741
+ } else {
3742
+ attempt.failed = true;
3743
+ state.status = code === 0 ? 'exited' : 'failed';
3744
+ }
3745
+ state.exitCode = Number.isFinite(code) ? code : null;
3746
+ state.signal = signal || '';
3747
+ state.exitedAt = new Date().toISOString();
3748
+ state.updatedAt = state.exitedAt;
3749
+ if (code !== 0 && !state.lastError) {
3750
+ state.lastError = signal ? `signal:${signal}` : `exit:${code}`;
3751
+ }
3752
+ attempt.error = attempt.error || state.lastError || `exit:${code ?? 'unknown'}`;
3753
+ });
3754
+ } catch (err) {
3755
+ attempt.failed = true;
3756
+ attempt.error = err?.message || String(err);
3757
+ state.status = 'failed';
3758
+ state.lastError = attempt.error;
3759
+ state.updatedAt = new Date().toISOString();
3760
+ }
3761
+
3762
+ return attempt;
3763
+ }
3764
+
3765
+ async function stopRemoteAgentAttempt(attempt, reason = 'race-lost') {
3766
+ if (!attempt?.child) {
3767
+ return;
3768
+ }
3769
+
3770
+ if (attempt.child.exitCode !== null || attempt.child.killed) {
3771
+ return;
3772
+ }
3773
+
3774
+ try {
3775
+ await terminateProcessTree(attempt.child);
3776
+ } catch (err) {
3777
+ logWarn('remote', `managed RemoteAgent cleanup failed ${formatKeyValue('manager', attempt.manager || '-')} ${formatKeyValue('reason', reason)} ${formatKeyValue('error', err?.message || err)}`);
3778
+ }
3779
+ }
3780
+
3781
+ async function startRemoteAgentConnectionRace(options = {}) {
3782
+ const managers = normalizeRemoteManagerEndpointList(options.managers).slice(0, REMOTE_AGENT_MAX_PARALLEL_CANDIDATES);
3783
+ const pairToken = safeRemoteAgentField(options.pairToken || options.pair, 512);
3784
+ const leaseId = safeRemoteAgentField(options.leaseId, 128);
3785
+ const nodeId = safeRemoteAgentField(options.nodeId, 128);
3786
+ const source = safeRemoteAgentField(options.source || 'registry', 64);
3787
+ const engine = normalizeRemoteAgentEngine(options.engine);
3788
+ const launcher = resolveRemoteAgentLauncher(engine);
3789
+ if (launcher?.ok === false) {
3790
+ const error = launcher.error || 'remote-agent-launcher-unavailable';
3791
+ remoteAgentState = createRemoteAgentIdleState({
3792
+ status: 'failed',
3793
+ manager: managers[0] || '',
3794
+ managerCandidates: managers,
3795
+ leaseId,
3796
+ nodeId,
3797
+ engine,
3798
+ source,
3799
+ lastError: error,
3800
+ updatedAt: new Date().toISOString()
3801
+ });
3802
+ emitBridgeEvent('RemoteAgentFailed', serializeRemoteAgentState());
3803
+ return { ok: false, error, agent: serializeRemoteAgentState() };
3804
+ }
3805
+
3806
+ remoteAgentState = createRemoteAgentIdleState({
3807
+ status: 'starting',
3808
+ manager: managers[0] || '',
3809
+ managerCandidates: managers,
3810
+ leaseId,
3811
+ nodeId,
3812
+ engine,
3813
+ source,
3814
+ startedAt: new Date().toISOString(),
3815
+ launcher: 'race',
3816
+ usingNpx: false
3817
+ });
3818
+
3819
+ const attempts = [];
3389
3820
  for (let index = 0; index < managers.length; index += 1) {
3390
3821
  const manager = managers[index];
3391
- if (managers.length > 1) {
3392
- logEvent(
3393
- 'remote',
3394
- `managed RemoteAgent candidate ${index + 1}/${managers.length} ${formatKeyValue('manager', manager)}`,
3395
- 'remote');
3396
- }
3822
+ logEvent(
3823
+ 'remote',
3824
+ `managed RemoteAgent race ${index + 1}/${managers.length} ${formatKeyValue('manager', manager)}`,
3825
+ 'remote');
3397
3826
 
3398
- const result = await startRemoteAgentConnectionAttempt({
3827
+ const attempt = createRemoteAgentAttempt({
3399
3828
  manager,
3400
3829
  managerCandidates: managers,
3401
3830
  pairToken,
3402
3831
  leaseId,
3403
3832
  nodeId,
3404
3833
  source,
3405
- engine
3834
+ engine,
3835
+ launcher
3406
3836
  });
3407
- if (result?.ok === true) {
3408
- return result;
3837
+ attempts.push(attempt);
3838
+
3839
+ if (index + 1 < managers.length && REMOTE_AGENT_RACE_START_STAGGER_MS > 0) {
3840
+ await new Promise(resolve => setTimeout(resolve, REMOTE_AGENT_RACE_START_STAGGER_MS));
3409
3841
  }
3842
+ }
3410
3843
 
3411
- lastResult = result;
3412
- if (isRemoteAgentProcessRunning()) {
3413
- await stopRemoteAgentConnection(`candidate-failed:${manager}`);
3844
+ const startedAt = Date.now();
3845
+ let winner = null;
3846
+ while (Date.now() - startedAt < REMOTE_AGENT_READY_TIMEOUT_MS) {
3847
+ winner = attempts.find(attempt =>
3848
+ attempt?.state?.ready === true
3849
+ && attempt?.child
3850
+ && attempt.child.exitCode === null
3851
+ && !attempt.child.killed);
3852
+ if (winner) {
3853
+ break;
3414
3854
  }
3415
3855
 
3416
- if (index + 1 < managers.length) {
3417
- logWarn(
3418
- 'remote',
3419
- `managed RemoteAgent candidate failed; trying next ${formatKeyValue('manager', manager)} ${formatKeyValue('error', result?.error || 'unknown')}`);
3856
+ const allFinished = attempts.every(attempt =>
3857
+ attempt.failed === true
3858
+ || !attempt.child
3859
+ || attempt.child.exitCode !== null
3860
+ || attempt.child.killed);
3861
+ if (allFinished) {
3862
+ break;
3420
3863
  }
3864
+
3865
+ await new Promise(resolve => setTimeout(resolve, 60));
3421
3866
  }
3422
3867
 
3868
+ if (winner) {
3869
+ for (const attempt of attempts) {
3870
+ if (attempt !== winner) {
3871
+ await stopRemoteAgentAttempt(attempt, 'race-lost');
3872
+ }
3873
+ }
3874
+
3875
+ remoteAgentState = winner.state;
3876
+ remoteAgentState.status = 'running';
3877
+ remoteAgentState.proc = winner.child;
3878
+ remoteAgentState.updatedAt = new Date().toISOString();
3879
+ rememberRemoteAgentSuccessfulManager(winner.manager, leaseId);
3880
+ emitBridgeEvent('RemoteAgentStarted', serializeRemoteAgentState());
3881
+ logEvent(
3882
+ 'remote',
3883
+ `managed RemoteAgent connected ${formatKeyValue('manager', winner.manager)} ${formatKeyValue('launcher', winner.launcher?.launcher || '-')}`,
3884
+ 'remote');
3885
+ return { ok: true, alreadyRunning: false, agent: serializeRemoteAgentState() };
3886
+ }
3887
+
3888
+ const lastAttempt = attempts.findLast?.(attempt => attempt?.error || attempt?.state?.lastError)
3889
+ || attempts[attempts.length - 1]
3890
+ || null;
3891
+ for (const attempt of attempts) {
3892
+ await stopRemoteAgentAttempt(attempt, 'race-failed');
3893
+ }
3894
+
3895
+ const error = lastAttempt
3896
+ ? summarizeRemoteAgentAttempt(lastAttempt)
3897
+ : 'all-manager-candidates-failed';
3898
+ remoteAgentState = createRemoteAgentIdleState({
3899
+ status: 'failed',
3900
+ manager: managers[0] || '',
3901
+ managerCandidates: managers,
3902
+ leaseId,
3903
+ nodeId,
3904
+ engine,
3905
+ source,
3906
+ lastError: error,
3907
+ updatedAt: new Date().toISOString()
3908
+ });
3909
+ emitBridgeEvent('RemoteAgentFailed', serializeRemoteAgentState());
3423
3910
  return {
3424
3911
  ok: false,
3425
- error: lastResult?.error || 'all-manager-candidates-failed',
3426
- agent: lastResult?.agent || serializeRemoteAgentState()
3912
+ error,
3913
+ agent: serializeRemoteAgentState()
3427
3914
  };
3428
3915
  }
3429
3916
 
@@ -3445,17 +3932,11 @@ async function startRemoteAgentConnectionAttempt(options = {}) {
3445
3932
  }
3446
3933
 
3447
3934
  const connectionKey = createRemoteAgentConnectionKey(manager, leaseId);
3448
- const launcher = resolveRemoteAgentLauncher();
3449
- const args = [
3450
- ...launcher.argsPrefix,
3451
- 'connect',
3452
- '--manager',
3453
- manager,
3454
- '--pair',
3455
- pairToken,
3456
- '--engine',
3457
- engine
3458
- ];
3935
+ const launcher = resolveRemoteAgentLauncher(engine);
3936
+ if (launcher?.ok === false) {
3937
+ return { ok: false, error: launcher.error || 'remote-agent-launcher-unavailable', agent: serializeRemoteAgentState() };
3938
+ }
3939
+ const args = buildRemoteAgentLaunchArgs(launcher, manager, pairToken, engine);
3459
3940
 
3460
3941
  remoteAgentState = createRemoteAgentIdleState({
3461
3942
  status: 'starting',
@@ -8065,7 +8546,7 @@ app.get('/api/remote/status', (req, res) => {
8065
8546
 
8066
8547
  app.get('/api/remote/devices', (req, res) => {
8067
8548
  res.setHeader('Cache-Control', 'no-store');
8068
- const includeDataUrl = !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8549
+ const includeDataUrl = /^(1|true|yes|on|data|base64)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8069
8550
  const devices = remoteHub.listDevices({ includeDataUrl });
8070
8551
  res.json({
8071
8552
  total: devices.length,
@@ -8285,7 +8766,7 @@ function isRemoteCapabilityEnabled(device, key) {
8285
8766
  }
8286
8767
 
8287
8768
  function includeRemoteFrameDataUrl(req) {
8288
- return !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8769
+ return /^(1|true|yes|on|data|base64)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
8289
8770
  }
8290
8771
 
8291
8772
  function sendRemoteFrameBinary(req, res, frameKind) {
@@ -13463,6 +13463,8 @@
13463
13463
  const REMOTE_FLEET_THUMBNAIL_FRAME_DECODE_TIMEOUT_MS = 1200;
13464
13464
  const REMOTE_FLEET_BINARY_FRAME_WS_RECONNECT_MS = 1500;
13465
13465
  const REMOTE_FLEET_BINARY_FRAME_SUBSCRIBE_MS = 1000;
13466
+ const REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL = 8;
13467
+ const REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS = 3000;
13466
13468
  const REMOTE_FLEET_TASK_FOLLOW_INITIAL_MS = 250;
13467
13469
  const REMOTE_FLEET_TASK_FOLLOW_REFRESH_MS = 2000;
13468
13470
  const REMOTE_FLEET_TASK_FOLLOW_MAX_TICKS = 60;
@@ -13760,15 +13762,13 @@
13760
13762
  const payload = buffer.slice(4 + metaLength);
13761
13763
  const mimeType = String(metadata.mimeType || metadata.format || 'image/jpeg').trim() || 'image/jpeg';
13762
13764
  const payloadBlob = new Blob([payload], { type: mimeType });
13763
- const objectUrl = URL.createObjectURL(payloadBlob);
13764
13765
  return {
13765
13766
  ...metadata,
13766
13767
  kind: String(metadata.kind || 'live').toLowerCase() === 'thumbnail' ? 'thumbnail' : 'live',
13767
13768
  mimeType,
13768
- frameUrl: objectUrl,
13769
- framePath: objectUrl,
13769
+ frameUrl: '',
13770
+ framePath: '',
13770
13771
  dataUrl: '',
13771
- _remoteFleetObjectUrl: objectUrl,
13772
13772
  _remoteFleetPayloadBlob: payloadBlob,
13773
13773
  _remoteFleetBinaryFrame: true
13774
13774
  };
@@ -14103,6 +14103,7 @@
14103
14103
  function isRemoteFleetFrameSource(value) {
14104
14104
  const source = String(value || '').trim();
14105
14105
  if (/^data:image\/(png|jpe?g|webp|svg\+xml);base64,/i.test(source)
14106
+ || /^blob:/i.test(source)
14106
14107
  || /^\/api\/remote\//i.test(source)) {
14107
14108
  return true;
14108
14109
  }
@@ -14370,11 +14371,26 @@
14370
14371
  }
14371
14372
 
14372
14373
  async function loadRemoteFleetFrameBitmap(frame) {
14373
- if (!frame || !isRemoteFleetFrameSource(frame.frameUrl)) {
14374
+ if (!frame) {
14375
+ return null;
14376
+ }
14377
+
14378
+ if (typeof createImageBitmap !== 'function') {
14379
+ return null;
14380
+ }
14381
+
14382
+ const timeoutMs = getRemoteFleetFrameDecodeTimeoutMs(frame);
14383
+ if (frame._remoteFleetPayloadBlob) {
14384
+ return await withRemoteFleetFrameTimeout(
14385
+ createImageBitmap(frame._remoteFleetPayloadBlob),
14386
+ timeoutMs);
14387
+ }
14388
+
14389
+ if (!isRemoteFleetFrameSource(frame.frameUrl)) {
14374
14390
  return null;
14375
14391
  }
14376
14392
 
14377
- if (typeof fetch !== 'function' || typeof createImageBitmap !== 'function') {
14393
+ if (typeof fetch !== 'function') {
14378
14394
  return null;
14379
14395
  }
14380
14396
 
@@ -14412,7 +14428,6 @@
14412
14428
  entry.expiresAt = now + REMOTE_FLEET_FRAME_BLOB_CACHE_MS;
14413
14429
  }
14414
14430
 
14415
- const timeoutMs = getRemoteFleetFrameDecodeTimeoutMs(frame);
14416
14431
  const blob = await withRemoteFleetFrameTimeout(
14417
14432
  entry.promise,
14418
14433
  timeoutMs,
@@ -14426,12 +14441,12 @@
14426
14441
  String(frame?.kind || '').trim().toLowerCase(),
14427
14442
  String(frame?.streamId || '').trim(),
14428
14443
  String(Number(frame?.frameSeq || 0) || 0),
14429
- String(frame?.frameUrl || '').trim()
14444
+ String(frame?.contentHash || frame?.frameUrl || '').trim()
14430
14445
  ].join('|');
14431
14446
  }
14432
14447
 
14433
14448
  function cloneRemoteFleetFramePatch(frame) {
14434
- return {
14449
+ const patch = {
14435
14450
  deviceId: String(frame?.deviceId || '').trim(),
14436
14451
  kind: String(frame?.kind || 'thumbnail').trim().toLowerCase(),
14437
14452
  frameSeq: Number.isFinite(Number(frame?.frameSeq)) ? Math.floor(Number(frame.frameSeq)) : 0,
@@ -14443,10 +14458,22 @@
14443
14458
  captureMs: Number(frame?.captureMs || 0) || 0,
14444
14459
  sameContentStreak: Number(frame?.sameContentStreak || 0) || 0
14445
14460
  };
14461
+ if (frame?._remoteFleetPayloadBlob) {
14462
+ patch._remoteFleetPayloadBlob = frame._remoteFleetPayloadBlob;
14463
+ }
14464
+ if (frame?._remoteFleetObjectUrl) {
14465
+ patch._remoteFleetObjectUrl = frame._remoteFleetObjectUrl;
14466
+ }
14467
+ if (frame?._remoteFleetBinaryFrame === true) {
14468
+ patch._remoteFleetBinaryFrame = true;
14469
+ }
14470
+ return patch;
14446
14471
  }
14447
14472
 
14448
14473
  function isRemoteFleetFrameNewerForPreview(preview, frame, comparePending = false) {
14449
- if (!preview || !frame || !isRemoteFleetFrameSource(frame.frameUrl)) {
14474
+ const hasPayloadBlob = !!frame?._remoteFleetPayloadBlob;
14475
+ const hasFrameSource = isRemoteFleetFrameSource(frame?.frameUrl);
14476
+ if (!preview || !frame || (!hasPayloadBlob && !hasFrameSource)) {
14450
14477
  return false;
14451
14478
  }
14452
14479
 
@@ -14543,23 +14570,7 @@
14543
14570
  placeholder.remove();
14544
14571
  }
14545
14572
 
14546
- const previewMode = String(preview?.dataset?.remoteFleetDevicePreview || '');
14547
- if (previewMode === 'tile') {
14548
- preview.querySelector?.('[data-remote-fleet-frame-badge="true"]')?.remove();
14549
- return true;
14550
- }
14551
-
14552
- const badge = ensureRemoteFleetFrameBadge(preview, frame);
14553
- if (badge) {
14554
- const at = frame.receivedAt || frame.capturedAt || '';
14555
- badge.textContent = frame.kind === 'live'
14556
- ? `LIVE ${at ? formatRemoteFleetAge(at) : ''}`.trim()
14557
- : (at ? formatRemoteFleetAge(at) : 'Screen');
14558
- badge.style.display = 'inline-flex';
14559
- badge.style.background = frame.kind === 'live'
14560
- ? 'rgba(220, 38, 38, 0.84)'
14561
- : 'rgba(15, 23, 42, 0.72)';
14562
- }
14573
+ preview.querySelector?.('[data-remote-fleet-frame-badge="true"]')?.remove();
14563
14574
 
14564
14575
  return true;
14565
14576
  }
@@ -14631,7 +14642,7 @@
14631
14642
  }
14632
14643
 
14633
14644
  const frame = preview._remoteFleetPendingFrame;
14634
- if (!frame || !isRemoteFleetFrameSource(frame.frameUrl)) {
14645
+ if (!frame || (!frame._remoteFleetPayloadBlob && !isRemoteFleetFrameSource(frame.frameUrl))) {
14635
14646
  clearRemoteFleetPendingFrameDataset(preview, frame);
14636
14647
  return;
14637
14648
  }
@@ -14668,6 +14679,25 @@
14668
14679
  return;
14669
14680
  }
14670
14681
 
14682
+ let fallbackUrl = String(frame.frameUrl || '').trim();
14683
+ if (!isRemoteFleetFrameSource(fallbackUrl)
14684
+ && frame._remoteFleetPayloadBlob
14685
+ && typeof URL !== 'undefined'
14686
+ && typeof URL.createObjectURL === 'function') {
14687
+ try {
14688
+ fallbackUrl = URL.createObjectURL(frame._remoteFleetPayloadBlob);
14689
+ frame.frameUrl = fallbackUrl;
14690
+ frame._remoteFleetObjectUrl = fallbackUrl;
14691
+ } catch {
14692
+ fallbackUrl = '';
14693
+ }
14694
+ }
14695
+
14696
+ if (!isRemoteFleetFrameSource(fallbackUrl)) {
14697
+ finish(false, 'img-unavailable');
14698
+ return;
14699
+ }
14700
+
14671
14701
  const timeoutMs = getRemoteFleetFrameDecodeTimeoutMs(frame);
14672
14702
  let settled = false;
14673
14703
  const complete = (loaded, surface = 'img') => {
@@ -14686,7 +14716,7 @@
14686
14716
  decoded.then(() => complete(true, 'img'));
14687
14717
  };
14688
14718
  loader.onerror = () => complete(false, 'img');
14689
- loader.src = frame.frameUrl;
14719
+ loader.src = fallbackUrl;
14690
14720
  };
14691
14721
 
14692
14722
  const canvas = ensureRemoteFleetFrameCanvas(preview);
@@ -14748,7 +14778,7 @@
14748
14778
  seq: frame.frameSeq,
14749
14779
  error: error?.message || String(error || '')
14750
14780
  });
14751
- finish(false, 'canvas-timeout');
14781
+ loadFallbackImage();
14752
14782
  return;
14753
14783
  }
14754
14784
 
@@ -14880,11 +14910,10 @@
14880
14910
 
14881
14911
  function applyRemoteFleetFramePatchToPreview(preview, frame) {
14882
14912
  if (String(frame?.kind || '').toLowerCase() === 'live') {
14883
- preview._remoteFleetPendingFrame = null;
14884
- delete preview.dataset.remoteFleetPendingFrameKind;
14885
- delete preview.dataset.remoteFleetPendingFrameSeq;
14886
- delete preview.dataset.remoteFleetPendingFrameUrl;
14887
- return queueRemoteFleetImageSwap(preview, frame);
14913
+ preview._remoteFleetPendingImageFrame = null;
14914
+ delete preview.dataset.remoteFleetPendingImageKind;
14915
+ delete preview.dataset.remoteFleetPendingImageSeq;
14916
+ delete preview.dataset.remoteFleetPendingImageUrl;
14888
14917
  }
14889
14918
 
14890
14919
  return queueRemoteFleetFramePaint(preview, frame);
@@ -14964,6 +14993,16 @@
14964
14993
  return;
14965
14994
  }
14966
14995
 
14996
+ const binarySession = bodyView._remoteFleetBinaryFrameSession;
14997
+ const binarySocket = binarySession?.ws || null;
14998
+ if (binarySocket
14999
+ && typeof WebSocket !== 'undefined'
15000
+ && (binarySocket.readyState === WebSocket.OPEN || binarySocket.readyState === WebSocket.CONNECTING)) {
15001
+ lastTick = timestamp;
15002
+ bodyView._remoteFleetFrameLoopRaf = requestRemoteFleetFrameLoopFrame(loop);
15003
+ return;
15004
+ }
15005
+
14967
15006
  if (timestamp - lastTick >= intervalMs && bodyView._remoteFleetFrameRefreshInFlight !== true) {
14968
15007
  lastTick = timestamp;
14969
15008
  bodyView._remoteFleetFrameRefreshInFlight = true;
@@ -15993,29 +16032,6 @@
15993
16032
  `;
15994
16033
  preview.appendChild(placeholder);
15995
16034
  }
15996
- const previewBadge = document.createElement('span');
15997
- previewBadge.dataset.remoteFleetFrameBadge = 'true';
15998
- previewBadge.textContent = hasLiveFrame
15999
- ? `LIVE ${previewAt ? formatRemoteFleetAge(previewAt) : ''}`.trim()
16000
- : (previewAt ? formatRemoteFleetAge(previewAt) : (hubStatus === 'online' ? 'Pinned' : 'Hub offline'));
16001
- previewBadge.style.cssText = `
16002
- position: absolute;
16003
- left: 7px;
16004
- bottom: 7px;
16005
- max-width: calc(100% - 14px);
16006
- padding: 4px 7px;
16007
- border-radius: 999px;
16008
- background: ${hasLiveFrame ? 'rgba(220, 38, 38, 0.84)' : 'rgba(15, 23, 42, 0.74)'};
16009
- color: #e2e8f0;
16010
- font-size: 9px;
16011
- font-weight: 950;
16012
- line-height: 1;
16013
- overflow: hidden;
16014
- text-overflow: ellipsis;
16015
- white-space: nowrap;
16016
- letter-spacing: 0;
16017
- `;
16018
- preview.appendChild(previewBadge);
16019
16035
  bodyView.appendChild(preview);
16020
16036
 
16021
16037
  const stats = document.createElement('div');
@@ -17109,35 +17125,39 @@
17109
17125
  return ids.slice(0, 120);
17110
17126
  };
17111
17127
  const ensureVisibleRemoteFleetLiveStreams = async () => {
17112
- if (!(bodyView._remoteFleetAutoLiveStartedIds instanceof Set)) {
17113
- bodyView._remoteFleetAutoLiveStartedIds = new Set();
17128
+ if (!(bodyView._remoteFleetAutoLiveStartInFlight instanceof Set)) {
17129
+ bodyView._remoteFleetAutoLiveStartInFlight = new Set();
17114
17130
  }
17131
+ if (!(bodyView._remoteFleetAutoLiveStartAttemptAt instanceof Map)) {
17132
+ bodyView._remoteFleetAutoLiveStartAttemptAt = new Map();
17133
+ }
17134
+ const now = Date.now();
17115
17135
  const targets = getVisibleLiveDevices()
17116
17136
  .filter(device => {
17117
17137
  const deviceId = getRemoteFleetDeviceId(device);
17138
+ const lastAttemptAt = Number(bodyView._remoteFleetAutoLiveStartAttemptAt.get(deviceId) || 0);
17118
17139
  return deviceId
17119
17140
  && !isRemoteFleetLiveActive(device)
17120
- && !bodyView._remoteFleetAutoLiveStartedIds.has(deviceId);
17141
+ && !bodyView._remoteFleetAutoLiveStartInFlight.has(deviceId)
17142
+ && (now - lastAttemptAt >= REMOTE_FLEET_AUTO_LIVE_START_COOLDOWN_MS);
17121
17143
  });
17122
17144
  if (targets.length === 0) {
17123
17145
  return null;
17124
17146
  }
17125
17147
 
17126
- if (!(bodyView._remoteFleetAutoLiveStartInFlight instanceof Set)) {
17127
- bodyView._remoteFleetAutoLiveStartInFlight = new Set();
17128
- }
17129
-
17130
17148
  let started = 0;
17131
17149
  let lastResult = null;
17132
- for (const target of targets) {
17150
+ const queue = targets.slice(0, 120);
17151
+ const startOne = async target => {
17133
17152
  const deviceId = getRemoteFleetDeviceId(target);
17134
17153
  if (!deviceId || bodyView._remoteFleetAutoLiveStartInFlight.has(deviceId)) {
17135
- continue;
17154
+ return;
17136
17155
  }
17137
17156
 
17138
17157
  bodyView._remoteFleetAutoLiveStartInFlight.add(deviceId);
17158
+ bodyView._remoteFleetAutoLiveStartAttemptAt.set(deviceId, Date.now());
17139
17159
  try {
17140
- const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId);
17160
+ const result = await invokeDotNetAsync('StartRemoteFleetLiveStreamFromJs', nodeId, deviceId, REMOTE_FLEET_MONITOR_LIVE_FPS);
17141
17161
  lastResult = result;
17142
17162
  const success = isRemoteFleetResultSuccess(result);
17143
17163
  window.RuntimeTrace?.emit?.('remote.live.autoStart', {
@@ -17150,12 +17170,21 @@
17150
17170
  });
17151
17171
  if (success) {
17152
17172
  started += 1;
17153
- bodyView._remoteFleetAutoLiveStartedIds.add(deviceId);
17154
17173
  }
17155
17174
  } finally {
17156
17175
  bodyView._remoteFleetAutoLiveStartInFlight.delete(deviceId);
17157
17176
  }
17158
- }
17177
+ };
17178
+
17179
+ const workerCount = Math.min(REMOTE_FLEET_AUTO_LIVE_START_MAX_PARALLEL, queue.length);
17180
+ await Promise.all(Array.from({ length: workerCount }, async () => {
17181
+ while (queue.length > 0) {
17182
+ const target = queue.shift();
17183
+ if (target) {
17184
+ await startOne(target);
17185
+ }
17186
+ }
17187
+ }));
17159
17188
 
17160
17189
  if (started > 0) {
17161
17190
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-Sm5LEcaIEbOQMLRdM7FmN29gN1AP1yTcQOVH+E/ZgGY=",
4
+ "hash": "sha256-o8W1U0lBmrbs5unXcwG+/IEh2KogSA7Rk3MqjQnNfGU=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -131,7 +131,7 @@
131
131
  "MindExecution.Plugins.Directory.jc0g7fvyzv.dll": "MindExecution.Plugins.Directory.dll",
132
132
  "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "MindExecution.Plugins.PlanMaster.dll",
133
133
  "MindExecution.Plugins.YouTube.vy0nsbcoo4.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.e8lipb57xv.dll": "MindExecution.Shared.dll",
134
+ "MindExecution.Shared.uwkrc41o75.dll": "MindExecution.Shared.dll",
135
135
  "MindExecution.Web.994sb3nzuy.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
@@ -282,7 +282,7 @@
282
282
  "MindExecution.Kernel.3zom09ir6j.dll": "sha256-78vR2SNkF9FwdQv0fEItSyz4HzgyzwGUROJAnpripPA=",
283
283
  "MindExecution.Plugins.Concept.lxunwcdjgq.dll": "sha256-WVyvjaxW6UoGQvGW0icChLsXwYVCcdn0BLTftzlkCKc=",
284
284
  "MindExecution.Plugins.PlanMaster.2sugb66h95.dll": "sha256-Lp0AieRHDo2H4Yl0Il1pPruy9nFUHJsX9t1AblXGD6Y=",
285
- "MindExecution.Shared.e8lipb57xv.dll": "sha256-Au9QMwnm9MaP2+xzdldeQYtj4tRXzwOBWYOxmYR8spM=",
285
+ "MindExecution.Shared.uwkrc41o75.dll": "sha256-YjF+7VcGo3+nB0fsa2nh3AiaRyrHS1AL6d6Mq4RWKqo=",
286
286
  "MindExecution.Web.994sb3nzuy.dll": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg="
287
287
  },
288
288
  "lazyAssembly": {
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-template-mdm-input-routing-v574" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-template-mdm-input-routing-v574" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260616-mdm-live-paint-v576" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260616-mdm-live-paint-v576" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260616-template-mdm-input-routing-v574';
582
+ const scriptVersion = '20260616-mdm-live-paint-v576';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "j2+Rgs1I",
2
+ "version": "dvBBmOSo",
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-x6IqfFnk+HJevHd67L05oQLv68rKhUOs+PFwvnpYjp4=",
89
+ "hash": "sha256-f0C7hF8MEs79aW0LHHOPm+FeBP7cvy/GxhypXzccmd4=",
90
90
  "url": "_content/MindExecution.Shared/js/mind-map-css3d-manager.js"
91
91
  },
92
92
  {
@@ -442,8 +442,8 @@
442
442
  "url": "_framework/MindExecution.Plugins.YouTube.vy0nsbcoo4.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-Au9QMwnm9MaP2+xzdldeQYtj4tRXzwOBWYOxmYR8spM=",
446
- "url": "_framework/MindExecution.Shared.e8lipb57xv.dll"
445
+ "hash": "sha256-YjF+7VcGo3+nB0fsa2nh3AiaRyrHS1AL6d6Mq4RWKqo=",
446
+ "url": "_framework/MindExecution.Shared.uwkrc41o75.dll"
447
447
  },
448
448
  {
449
449
  "hash": "sha256-jQshtLmhEC4xenzdD9PqHjaCuKvJf/frzaKBi2weDAg=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-XSS0rv+S2JAW90BVDbFUd23REh87EAC1P5aFjXSMAGo=",
773
+ "hash": "sha256-lL/HCjpHZ3+EWJkTvNj72sH8NLl/eSU2UZDTr6bP99c=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-A1rOSgC5wEjKNEOppREqJ/PkL33qbguPos0pVcG6L/4=",
837
+ "hash": "sha256-HElWHU9ioUT0cuuCKMD/40OJ15fyVOJNYjuApu3T4mI=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: j2+Rgs1I */
1
+ /* Manifest version: dvBBmOSo */
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