@livedesk/hub 0.1.40 → 0.1.42

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.
@@ -8,11 +8,11 @@ export const LIVE_DESK_UPDATE_COMMAND = 'livedesk.client-update';
8
8
  // same device returned, so the Hub-owned compatibility bridge upgrades them.
9
9
  export const LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION = '0.1.172';
10
10
  export const LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE = 5;
11
- export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 480_000;
12
- export const LIVE_DESK_UPDATE_TIMEOUT_MS = 90 * 60_000;
13
- export const LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS = 60_000;
14
- export const LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS = 10_000;
15
- export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
11
+ export const LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS = 480_000;
12
+ export const LIVE_DESK_UPDATE_TIMEOUT_MS = 90 * 60_000;
13
+ export const LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS = 60_000;
14
+ export const LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS = 10_000;
15
+ export const LIVE_DESK_UPDATE_CHECK_INTERVAL_MS = 60_000;
16
16
  export const LIVE_DESK_LEGACY_COMMAND_MAX_LENGTH = 3900;
17
17
 
18
18
  function cleanVersion(value) {
@@ -66,13 +66,17 @@ export function isVersionAtLeast(candidate, required) {
66
66
  return !!cleanVersion(candidate) && !!cleanVersion(required) && compareVersions(candidate, required) >= 0;
67
67
  }
68
68
 
69
+ function isDedicatedBootstrapFailure(value) {
70
+ return /^Update worker exited before terminal proof\b/i.test(String(value || '').trim());
71
+ }
72
+
69
73
  function encodePowerShell(value) {
70
74
  return Buffer.from(String(value || ''), 'utf16le').toString('base64');
71
75
  }
72
76
 
73
77
  function buildLegacyPackageSupervisorStarterSource() {
74
78
  const source = [
75
- "const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(require('node:os').tmpdir(),'livedesk-update-cwd','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','--prefix',w,'--workspaces=false','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);Object.keys(e).forEach(k=>/^(INIT_CWD|npm_config_(local_prefix|workspaces?|include_workspace_root))$/i.test(k)&&Reflect.deleteProperty(e,k));Object.assign(e,{INIT_CWD:w,npm_config_local_prefix:w,npm_config_workspaces:'false',npm_config_include_workspace_root:'false',LIVEDESK_UPDATE_NEUTRAL_CWD:w});e.i=x?{c:j,a:[x,...a]}:s==='win32'?{c:e.ComSpec||'cmd.exe',a:['/d','/s','/c','call'+z+q(n)+z+a.map(q).join(z)]}:{c:n,a};e.r=spawn(e.i.c,e.i.a,{cwd:w,env:e,detached:true,stdio:'ignore',windowsHide:true});",
79
+ "const{spawn}=require('node:child_process'),f=require('node:fs'),p=require('node:path'),o=require('node:os'),c=JSON.parse(Buffer.from(process.env.C,'base64')),e={...process.env,LIVEDESK_UPDATE_STARTER_PID:String(process.pid),LIVEDESK_UPDATE_ORIGINAL_CWD:process.cwd()},w=p.join(o.tmpdir(),'livedesk-update-cwd','client-'+c[0]),k=p.join(o.tmpdir(),'livedesk-update-cache','client-'+c[0]),u=e.LIVEDESK_NPX_EXECUTABLE,s=process.platform,j=process.execPath,n=u||(s==='win32'?'npx.cmd':'npx'),x=[e.LIVEDESK_NPX_CLI_PATH,e.npm_execpath&&p.join(p.dirname(e.npm_execpath),'npx-cli.js'),u&&p.join(p.dirname(u),'node_modules/npm/bin/npx-cli.js'),p.join(p.dirname(j),'node_modules/npm/bin/npx-cli.js')].find(v=>v&&f.existsSync(v)),a=['-y','--prefer-online','--prefix',w,'--workspaces=false','livedesk@'+c[6],'--internal-legacy-client-update'],z=String.fromCharCode(32),q=v=>'\"'+String(v).replaceAll('\"','\"\"')+'\"';if(!(+c[7]>0))process.exit(1);c[7]=String(Date.now()+Number(c[7]));e.C=Buffer.from(JSON.stringify(c)).toString('base64');f.mkdirSync(w,{recursive:true});if(f.readdirSync(w)[0])process.exit(1);f.mkdirSync(k,{recursive:true});Object.keys(e).forEach(n=>/^(INIT_CWD|npm_config_(cache|local_prefix|workspaces?|include_workspace_root))$/i.test(n)&&Reflect.deleteProperty(e,n));Object.assign(e,{INIT_CWD:w,npm_config_cache:k,npm_config_local_prefix:w,npm_config_workspaces:'false',npm_config_include_workspace_root:'false',LIVEDESK_UPDATE_NEUTRAL_CWD:w});e.i=x?{c:j,a:[x,...a]}:s==='win32'?{c:e.ComSpec||'cmd.exe',a:['/d','/s','/c','call'+z+q(n)+z+a.map(q).join(z)]}:{c:n,a};e.r=spawn(e.i.c,e.i.a,{cwd:w,env:e,detached:true,stdio:'ignore',windowsHide:true});",
76
80
  "e.r.on('error',()=>process.exit(1));e.r.unref();"
77
81
  ].join('');
78
82
  if (source.includes(' ')) throw new Error('LiveDesk legacy starter source must not contain spaces.');
@@ -195,10 +199,10 @@ function statusForRun(run) {
195
199
  activeCount,
196
200
  queuedCount,
197
201
  pendingOfflineCount,
198
- failedCount,
199
- batchSize: run.batchSize,
200
- clientRestartStabilityMs: run.clientRestartStabilityMs,
201
- verifiedClientRestartStabilityMs: run.verifiedClientRestartStabilityMs,
202
+ failedCount,
203
+ batchSize: run.batchSize,
204
+ clientRestartStabilityMs: run.clientRestartStabilityMs,
205
+ verifiedClientRestartStabilityMs: run.verifiedClientRestartStabilityMs,
202
206
  latestManagerVersion: run.latestManagerVersion,
203
207
  latestClientVersion: run.latestClientVersion,
204
208
  error: run.error || '',
@@ -228,35 +232,45 @@ function resetReconnectCandidate(target) {
228
232
  target.candidatePid = 0;
229
233
  target.candidateConnectedAt = '';
230
234
  target.candidateProductVersion = '';
231
- target.candidateAgentVersion = '';
232
- target.candidateSupervisorProofId = '';
233
- target.verificationSource = '';
234
- target.requiredStabilityMs = 0;
235
- target.supervisorProofAt = '';
236
- target.candidateSince = '';
237
- target.stabilityDeadlineAt = '';
238
- }
239
-
240
- export function createLiveDeskUpdateManager({
241
- remoteHub,
242
- currentManagerVersion,
243
- currentClientVersion,
235
+ target.candidateAgentVersion = '';
236
+ target.candidateSupervisorProofId = '';
237
+ target.verificationSource = '';
238
+ target.requiredStabilityMs = 0;
239
+ target.supervisorProofAt = '';
240
+ target.candidateSince = '';
241
+ target.stabilityDeadlineAt = '';
242
+ }
243
+
244
+ export function createLiveDeskUpdateManager({
245
+ remoteHub,
246
+ currentManagerVersion,
247
+ currentClientVersion,
248
+ excludedDeviceIds = [],
244
249
  restartSupported = false,
245
250
  requestHubRestart,
246
251
  fetchImpl = globalThis.fetch,
247
252
  now = () => Date.now(),
248
253
  clientBatchSize = LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE,
249
- targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
250
- operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
251
- clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS,
252
- verifiedClientRestartStabilityMs = LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
253
- }) {
254
+ targetTimeoutMs = LIVE_DESK_UPDATE_TARGET_TIMEOUT_MS,
255
+ operationTimeoutMs = LIVE_DESK_UPDATE_TIMEOUT_MS,
256
+ clientRestartStabilityMs = LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS,
257
+ verifiedClientRestartStabilityMs = LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
258
+ }) {
254
259
  let latestRelease = null;
255
260
  let checkError = '';
256
261
  let checkPromise = null;
257
262
  let run = null;
258
263
  let checkTimer = null;
259
- let runTimer = null;
264
+ let runTimer = null;
265
+ const excludedDeviceIdSet = new Set(
266
+ (Array.isArray(excludedDeviceIds) ? excludedDeviceIds : [excludedDeviceIds])
267
+ .map(value => String(value || '').trim())
268
+ .filter(Boolean)
269
+ );
270
+ const connectedClientDevices = () => connectedDevices(remoteHub)
271
+ .filter(device => !excludedDeviceIdSet.has(String(device?.deviceId || '')));
272
+ const knownOfflineClientDevices = () => knownOfflineDevices(remoteHub)
273
+ .filter(device => !excludedDeviceIdSet.has(String(device?.deviceId || '')));
260
274
  const effectiveClientBatchSize = Math.max(
261
275
  1,
262
276
  Math.min(50, Math.floor(Number(clientBatchSize) || LIVE_DESK_UPDATE_CLIENT_BATCH_SIZE))
@@ -269,18 +283,18 @@ export function createLiveDeskUpdateManager({
269
283
  effectiveTargetTimeoutMs,
270
284
  Number(operationTimeoutMs) || LIVE_DESK_UPDATE_TIMEOUT_MS
271
285
  );
272
- const effectiveClientRestartStabilityMs = Math.max(
286
+ const effectiveClientRestartStabilityMs = Math.max(
273
287
  0,
274
288
  Number.isFinite(Number(clientRestartStabilityMs))
275
289
  ? Number(clientRestartStabilityMs)
276
- : LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
277
- );
278
- const effectiveVerifiedClientRestartStabilityMs = Math.max(
279
- 5_000,
280
- Number.isFinite(Number(verifiedClientRestartStabilityMs))
281
- ? Number(verifiedClientRestartStabilityMs)
282
- : LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
283
- );
290
+ : LIVE_DESK_UPDATE_CLIENT_RESTART_STABILITY_MS
291
+ );
292
+ const effectiveVerifiedClientRestartStabilityMs = Math.max(
293
+ 5_000,
294
+ Number.isFinite(Number(verifiedClientRestartStabilityMs))
295
+ ? Number(verifiedClientRestartStabilityMs)
296
+ : LIVE_DESK_UPDATE_VERIFIED_CLIENT_STABILITY_MS
297
+ );
284
298
 
285
299
  const touch = () => {
286
300
  if (run) run.updatedAt = new Date(now()).toISOString();
@@ -307,7 +321,7 @@ export function createLiveDeskUpdateManager({
307
321
  const getStatus = () => {
308
322
  const managerUpdateAvailable = !!latestRelease
309
323
  && compareVersions(latestRelease.latestManagerVersion, currentManagerVersion) > 0;
310
- const clientDevices = connectedDevices(remoteHub);
324
+ const clientDevices = connectedClientDevices();
311
325
  const outdatedClientDevices = latestRelease
312
326
  ? clientDevices.filter(device => deviceNeedsUpdate(
313
327
  device,
@@ -316,7 +330,7 @@ export function createLiveDeskUpdateManager({
316
330
  ))
317
331
  : [];
318
332
  const pendingOfflineClientCount = latestRelease
319
- ? knownOfflineDevices(remoteHub).filter(device => deviceNeedsUpdate(
333
+ ? knownOfflineClientDevices().filter(device => deviceNeedsUpdate(
320
334
  device,
321
335
  latestRelease.latestManagerVersion,
322
336
  latestRelease.latestClientVersion
@@ -354,10 +368,10 @@ export function createLiveDeskUpdateManager({
354
368
  queuedCount: 0,
355
369
  pendingOfflineCount: 0,
356
370
  failedCount: 0,
357
- batchSize: effectiveClientBatchSize,
358
- clientRestartStabilityMs: effectiveClientRestartStabilityMs,
359
- verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
360
- targets: []
371
+ batchSize: effectiveClientBatchSize,
372
+ clientRestartStabilityMs: effectiveClientRestartStabilityMs,
373
+ verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
374
+ targets: []
361
375
  })
362
376
  };
363
377
  };
@@ -400,8 +414,9 @@ export function createLiveDeskUpdateManager({
400
414
  if (!run || run.state !== 'failed') return [];
401
415
  const currentTime = now();
402
416
  const devices = new Map(
403
- remoteHub.listDevices({ includeDataUrl: false })
404
- .map(device => [String(device.deviceId), device])
417
+ remoteHub.listDevices({ includeDataUrl: false })
418
+ .filter(device => !excludedDeviceIdSet.has(String(device?.deviceId || '')))
419
+ .map(device => [String(device.deviceId), device])
405
420
  );
406
421
  return run.targets.filter(target => {
407
422
  if (target.state !== 'failed' || !target.dispatchedAt) return false;
@@ -436,9 +451,16 @@ export function createLiveDeskUpdateManager({
436
451
  return true;
437
452
  };
438
453
 
439
- const dispatchTarget = (target, device) => {
454
+ const dispatchTarget = (target, device, options = {}) => {
440
455
  const dispatchedAtEpochMs = now();
441
- const updateDeadlineEpochMs = dispatchedAtEpochMs + effectiveTargetTimeoutMs;
456
+ const requestedTimeoutMs = Math.max(
457
+ 10_000,
458
+ Math.min(
459
+ effectiveTargetTimeoutMs,
460
+ Number(options.updateTimeoutMs) || effectiveTargetTimeoutMs
461
+ )
462
+ );
463
+ const updateDeadlineEpochMs = dispatchedAtEpochMs + requestedTimeoutMs;
442
464
  const dispatchedSessionId = String(device?.sessionId || '').trim();
443
465
  if (!dispatchedSessionId) {
444
466
  target.state = 'failed';
@@ -448,14 +470,15 @@ export function createLiveDeskUpdateManager({
448
470
  target.dispatchedSessionId = dispatchedSessionId;
449
471
  target.dispatchedPid = Math.max(0, Math.floor(Number(device?.pid) || 0));
450
472
  resetReconnectCandidate(target);
451
- const supportsDedicated = device.capabilities?.clientUpdate === true
473
+ const supportsDedicated = options.forceLegacy !== true
474
+ && device.capabilities?.clientUpdate === true
452
475
  && isVersionAtLeast(device.agentVersion, LIVE_DESK_DEDICATED_CLIENT_UPDATE_MIN_VERSION);
453
476
  const payload = {
454
477
  targetVersion: run.latestClientVersion,
455
478
  targetProductVersion: run.latestManagerVersion,
456
479
  managerVersion: run.latestManagerVersion,
457
480
  operationId: run.operationId,
458
- updateTimeoutMs: effectiveTargetTimeoutMs
481
+ updateTimeoutMs: requestedTimeoutMs
459
482
  };
460
483
  const result = supportsDedicated
461
484
  ? remoteHub.sendCommand(device.deviceId, { command: LIVE_DESK_UPDATE_COMMAND, payload })
@@ -465,7 +488,7 @@ export function createLiveDeskUpdateManager({
465
488
  run.operationId,
466
489
  run.latestClientVersion,
467
490
  run.latestManagerVersion,
468
- effectiveTargetTimeoutMs
491
+ requestedTimeoutMs
469
492
  ),
470
493
  timeoutMs: 30_000
471
494
  });
@@ -500,7 +523,7 @@ export function createLiveDeskUpdateManager({
500
523
  const activeCount = run.targets.filter(target => target.state === 'waiting').length;
501
524
  let availableSlots = Math.max(0, run.batchSize - activeCount);
502
525
  if (availableSlots <= 0) return true;
503
- const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
526
+ const devices = new Map(connectedClientDevices().map(device => [String(device.deviceId), device]));
504
527
  for (const target of run.targets) {
505
528
  if (availableSlots <= 0) break;
506
529
  if (!['queued', 'pending-offline'].includes(target.state)) continue;
@@ -539,7 +562,7 @@ export function createLiveDeskUpdateManager({
539
562
  failRun(`Timed out waiting for ${unfinishedCount} client(s) to finish the rollout.`);
540
563
  return;
541
564
  }
542
- const devices = new Map(connectedDevices(remoteHub).map(device => [String(device.deviceId), device]));
565
+ const devices = new Map(connectedClientDevices().map(device => [String(device.deviceId), device]));
543
566
  for (const target of run.targets) {
544
567
  if (target.state === 'failed' || target.state === 'completed') continue;
545
568
  const device = devices.get(target.deviceId);
@@ -549,30 +572,30 @@ export function createLiveDeskUpdateManager({
549
572
  const connectedAtEpochMs = Date.parse(connectedAt);
550
573
  const dispatchedAtEpochMs = Date.parse(target.dispatchedAt || '');
551
574
  const candidatePid = Math.max(0, Math.floor(Number(device?.pid) || 0));
552
- const candidateProductVersion = String(device?.productVersion || '');
553
- const candidateAgentVersion = String(device?.agentVersion || '');
554
- const supervisorProof = device?.clientUpdateProof;
555
- const supervisorProofId = [
556
- String(supervisorProof?.operationId || ''),
557
- String(supervisorProof?.attemptId || ''),
558
- String(supervisorProof?.sessionId || ''),
559
- String(supervisorProof?.agentPid || ''),
560
- String(supervisorProof?.launcherPid || ''),
561
- String(supervisorProof?.completedAt || '')
562
- ].join(':');
563
- const hasSupervisorProof = !!supervisorProof
564
- && String(supervisorProof.operationId || '') === run.operationId
565
- && !!String(supervisorProof.attemptId || '')
566
- && String(supervisorProof.sessionId || '') === sessionId
567
- && Number(supervisorProof.agentPid || 0) === candidatePid
568
- && Number(supervisorProof.launcherPid || 0) > 1
569
- && cleanVersion(supervisorProof.productVersion) === cleanVersion(candidateProductVersion)
570
- && cleanVersion(supervisorProof.agentVersion) === cleanVersion(candidateAgentVersion)
571
- && Date.parse(String(supervisorProof.receivedAt || '')) >= connectedAtEpochMs;
572
- const requiredStabilityMs = hasSupervisorProof
573
- ? run.verifiedClientRestartStabilityMs
574
- : run.clientRestartStabilityMs;
575
- const qualifiesForStability = device?.connected === true
575
+ const candidateProductVersion = String(device?.productVersion || '');
576
+ const candidateAgentVersion = String(device?.agentVersion || '');
577
+ const supervisorProof = device?.clientUpdateProof;
578
+ const supervisorProofId = [
579
+ String(supervisorProof?.operationId || ''),
580
+ String(supervisorProof?.attemptId || ''),
581
+ String(supervisorProof?.sessionId || ''),
582
+ String(supervisorProof?.agentPid || ''),
583
+ String(supervisorProof?.launcherPid || ''),
584
+ String(supervisorProof?.completedAt || '')
585
+ ].join(':');
586
+ const hasSupervisorProof = !!supervisorProof
587
+ && String(supervisorProof.operationId || '') === run.operationId
588
+ && !!String(supervisorProof.attemptId || '')
589
+ && String(supervisorProof.sessionId || '') === sessionId
590
+ && Number(supervisorProof.agentPid || 0) === candidatePid
591
+ && Number(supervisorProof.launcherPid || 0) > 1
592
+ && cleanVersion(supervisorProof.productVersion) === cleanVersion(candidateProductVersion)
593
+ && cleanVersion(supervisorProof.agentVersion) === cleanVersion(candidateAgentVersion)
594
+ && Date.parse(String(supervisorProof.receivedAt || '')) >= connectedAtEpochMs;
595
+ const requiredStabilityMs = hasSupervisorProof
596
+ ? run.verifiedClientRestartStabilityMs
597
+ : run.clientRestartStabilityMs;
598
+ const qualifiesForStability = device?.connected === true
576
599
  && !!sessionId
577
600
  && candidatePid > 1
578
601
  && sessionId !== target.dispatchedSessionId
@@ -581,10 +604,10 @@ export function createLiveDeskUpdateManager({
581
604
  const sameCandidate = qualifiesForStability
582
605
  && target.candidateSessionId === sessionId
583
606
  && target.candidatePid === candidatePid
584
- && target.candidateConnectedAt === connectedAt
585
- && target.candidateProductVersion === candidateProductVersion
586
- && target.candidateAgentVersion === candidateAgentVersion
587
- && target.candidateSupervisorProofId === (hasSupervisorProof ? supervisorProofId : '');
607
+ && target.candidateConnectedAt === connectedAt
608
+ && target.candidateProductVersion === candidateProductVersion
609
+ && target.candidateAgentVersion === candidateAgentVersion
610
+ && target.candidateSupervisorProofId === (hasSupervisorProof ? supervisorProofId : '');
588
611
 
589
612
  if (!qualifiesForStability) {
590
613
  resetReconnectCandidate(target);
@@ -592,18 +615,18 @@ export function createLiveDeskUpdateManager({
592
615
  target.candidateSessionId = sessionId;
593
616
  target.candidatePid = candidatePid;
594
617
  target.candidateConnectedAt = connectedAt;
595
- target.candidateProductVersion = candidateProductVersion;
596
- target.candidateAgentVersion = candidateAgentVersion;
597
- target.candidateSupervisorProofId = hasSupervisorProof ? supervisorProofId : '';
598
- target.verificationSource = hasSupervisorProof ? 'supervisor-proof' : 'hub-session';
599
- target.requiredStabilityMs = requiredStabilityMs;
600
- target.supervisorProofAt = hasSupervisorProof
601
- ? String(supervisorProof.receivedAt || supervisorProof.completedAt || '')
602
- : '';
603
- target.candidateSince = new Date(currentTime).toISOString();
604
- target.stabilityDeadlineAt = new Date(
605
- currentTime + requiredStabilityMs
606
- ).toISOString();
618
+ target.candidateProductVersion = candidateProductVersion;
619
+ target.candidateAgentVersion = candidateAgentVersion;
620
+ target.candidateSupervisorProofId = hasSupervisorProof ? supervisorProofId : '';
621
+ target.verificationSource = hasSupervisorProof ? 'supervisor-proof' : 'hub-session';
622
+ target.requiredStabilityMs = requiredStabilityMs;
623
+ target.supervisorProofAt = hasSupervisorProof
624
+ ? String(supervisorProof.receivedAt || supervisorProof.completedAt || '')
625
+ : '';
626
+ target.candidateSince = new Date(currentTime).toISOString();
627
+ target.stabilityDeadlineAt = new Date(
628
+ currentTime + requiredStabilityMs
629
+ ).toISOString();
607
630
  }
608
631
 
609
632
  if (qualifiesForStability
@@ -666,7 +689,7 @@ export function createLiveDeskUpdateManager({
666
689
  if (!release) return { ok: false, error: checkError || 'LiveDesk update check failed.', ...getStatus() };
667
690
  const managerNeedsUpdate = compareVersions(release.latestManagerVersion, currentManagerVersion) > 0;
668
691
  const clientPackageNeedsUpdate = compareVersions(release.latestClientVersion, currentClientVersion) > 0;
669
- const connected = connectedDevices(remoteHub);
692
+ const connected = connectedClientDevices();
670
693
  const outdatedClients = connected.filter(device => deviceNeedsUpdate(
671
694
  device,
672
695
  release.latestManagerVersion,
@@ -688,9 +711,9 @@ export function createLiveDeskUpdateManager({
688
711
  latestManagerVersion: release.latestManagerVersion,
689
712
  latestClientVersion: release.latestClientVersion,
690
713
  needsHubRestart,
691
- batchSize: effectiveClientBatchSize,
692
- clientRestartStabilityMs: effectiveClientRestartStabilityMs,
693
- verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
714
+ batchSize: effectiveClientBatchSize,
715
+ clientRestartStabilityMs: effectiveClientRestartStabilityMs,
716
+ verifiedClientRestartStabilityMs: effectiveVerifiedClientRestartStabilityMs,
694
717
  error: '',
695
718
  targets: targets.map(device => ({
696
719
  deviceId: String(device.deviceId || ''),
@@ -709,15 +732,18 @@ export function createLiveDeskUpdateManager({
709
732
  candidateSessionId: '',
710
733
  candidatePid: 0,
711
734
  candidateConnectedAt: '',
712
- candidateProductVersion: '',
713
- candidateAgentVersion: '',
714
- candidateSupervisorProofId: '',
715
- verificationSource: '',
716
- requiredStabilityMs: 0,
717
- supervisorProofAt: '',
718
- candidateSince: '',
735
+ candidateProductVersion: '',
736
+ candidateAgentVersion: '',
737
+ candidateSupervisorProofId: '',
738
+ verificationSource: '',
739
+ requiredStabilityMs: 0,
740
+ supervisorProofAt: '',
741
+ candidateSince: '',
719
742
  stabilityDeadlineAt: '',
720
743
  completedAt: '',
744
+ bootstrapRetryCount: 0,
745
+ bootstrapRetryAt: '',
746
+ bootstrapRetryReason: '',
721
747
  error: ''
722
748
  }))
723
749
  };
@@ -745,9 +771,9 @@ export function createLiveDeskUpdateManager({
745
771
 
746
772
  const handleRemoteEvent = (type, event) => {
747
773
  if (!run || run.state !== 'waiting-for-clients') return;
748
- if (type === 'RemoteDeviceConnected'
749
- || type === 'RemoteDeviceDisconnected'
750
- || type === 'RemoteClientUpdateVerified') {
774
+ if (type === 'RemoteDeviceConnected'
775
+ || type === 'RemoteDeviceDisconnected'
776
+ || type === 'RemoteClientUpdateVerified') {
751
777
  verifyTargets();
752
778
  return;
753
779
  }
@@ -760,8 +786,41 @@ export function createLiveDeskUpdateManager({
760
786
  || !['dedicated', 'legacy-command-run'].includes(target.method)) return;
761
787
  const result = event?.result;
762
788
  if (event?.error || result?.ok === false || result?.status === 'failed' || result?.status === 'rejected') {
789
+ const failure = String(event.error || result?.error || result?.message || 'client-update-command-failed').slice(0, 500);
790
+ if (target.method === 'dedicated'
791
+ && target.bootstrapRetryCount === 0
792
+ && isDedicatedBootstrapFailure(failure)) {
793
+ const retryDevice = connectedClientDevices()
794
+ .find(device => String(device.deviceId || '') === target.deviceId);
795
+ const remainingMs = Date.parse(target.deadlineAt || '') - now();
796
+ if (retryDevice?.connected === true && remainingMs >= 10_000) {
797
+ target.bootstrapRetryCount = 1;
798
+ target.bootstrapRetryAt = new Date(now()).toISOString();
799
+ target.bootstrapRetryReason = failure;
800
+ target.error = '';
801
+ let retryResult;
802
+ try {
803
+ retryResult = dispatchTarget(target, retryDevice, {
804
+ forceLegacy: true,
805
+ updateTimeoutMs: remainingMs
806
+ });
807
+ } catch (error) {
808
+ target.error = String(error?.message || error || 'client-update-bootstrap-retry-failed').slice(0, 500);
809
+ retryResult = false;
810
+ }
811
+ if (retryResult === true) {
812
+ touch();
813
+ return;
814
+ }
815
+ target.state = 'failed';
816
+ if (!target.error) target.error = 'client-update-bootstrap-retry-failed';
817
+ touch();
818
+ failRun(`Client ${target.deviceName || target.deviceId} bootstrap retry failed: ${target.error}`);
819
+ return;
820
+ }
821
+ }
763
822
  target.state = 'failed';
764
- target.error = String(event.error || result?.error || result?.message || 'client-update-command-failed').slice(0, 500);
823
+ target.error = failure;
765
824
  touch();
766
825
  failRun(target.method === 'dedicated'
767
826
  ? `Client ${target.deviceName || target.deviceId} rejected the update request: ${target.error}`
@@ -1,38 +1,38 @@
1
- export const LIVE_STREAM_MONITOR_INDEX_MIN = 0;
2
- export const LIVE_STREAM_MONITOR_INDEX_MAX = 63;
3
-
4
- export function parseExactLiveStreamMonitorIndex(value) {
5
- return typeof value === 'number'
6
- && Number.isInteger(value)
7
- && value >= LIVE_STREAM_MONITOR_INDEX_MIN
8
- && value <= LIVE_STREAM_MONITOR_INDEX_MAX
9
- ? value
10
- : null;
11
- }
12
-
13
- export function isReusedLiveStreamFrameReady(result, activeStream, expectedBinding) {
14
- const expectedMonitorIndex = parseExactLiveStreamMonitorIndex(expectedBinding?.monitorIndex);
15
- const activeMonitorIndex = parseExactLiveStreamMonitorIndex(activeStream?.monitorIndex);
16
- const latestFrame = activeStream?.latestFrame;
17
- const frameMonitorIndex = parseExactLiveStreamMonitorIndex(latestFrame?.monitorIndex);
18
- const expectedStreamId = String(expectedBinding?.streamId || '');
19
- const expectedCommandId = String(expectedBinding?.commandId || '');
20
- const expectedCaptureGeneration = Number(expectedBinding?.captureGeneration || 0);
21
-
22
- return result?.reused === true
23
- && result?.ready === true
24
- && Number(activeStream?.framesReceived || 0) > 0
25
- && !!expectedStreamId
26
- && String(activeStream?.streamId || '') === expectedStreamId
27
- && !!expectedCommandId
28
- && String(activeStream?.commandId || '') === expectedCommandId
29
- && Number.isInteger(expectedCaptureGeneration)
30
- && expectedCaptureGeneration > 0
31
- && Number(activeStream?.captureGeneration || 0) === expectedCaptureGeneration
32
- && latestFrame?.currentGenerationVerified === true
33
- && String(latestFrame?.commandId || '') === expectedCommandId
34
- && Number(latestFrame?.captureGeneration || 0) === expectedCaptureGeneration
35
- && expectedMonitorIndex !== null
36
- && activeMonitorIndex === expectedMonitorIndex
37
- && frameMonitorIndex === expectedMonitorIndex;
38
- }
1
+ export const LIVE_STREAM_MONITOR_INDEX_MIN = 0;
2
+ export const LIVE_STREAM_MONITOR_INDEX_MAX = 63;
3
+
4
+ export function parseExactLiveStreamMonitorIndex(value) {
5
+ return typeof value === 'number'
6
+ && Number.isInteger(value)
7
+ && value >= LIVE_STREAM_MONITOR_INDEX_MIN
8
+ && value <= LIVE_STREAM_MONITOR_INDEX_MAX
9
+ ? value
10
+ : null;
11
+ }
12
+
13
+ export function isReusedLiveStreamFrameReady(result, activeStream, expectedBinding) {
14
+ const expectedMonitorIndex = parseExactLiveStreamMonitorIndex(expectedBinding?.monitorIndex);
15
+ const activeMonitorIndex = parseExactLiveStreamMonitorIndex(activeStream?.monitorIndex);
16
+ const latestFrame = activeStream?.latestFrame;
17
+ const frameMonitorIndex = parseExactLiveStreamMonitorIndex(latestFrame?.monitorIndex);
18
+ const expectedStreamId = String(expectedBinding?.streamId || '');
19
+ const expectedCommandId = String(expectedBinding?.commandId || '');
20
+ const expectedCaptureGeneration = Number(expectedBinding?.captureGeneration || 0);
21
+
22
+ return result?.reused === true
23
+ && result?.ready === true
24
+ && Number(activeStream?.framesReceived || 0) > 0
25
+ && !!expectedStreamId
26
+ && String(activeStream?.streamId || '') === expectedStreamId
27
+ && !!expectedCommandId
28
+ && String(activeStream?.commandId || '') === expectedCommandId
29
+ && Number.isInteger(expectedCaptureGeneration)
30
+ && expectedCaptureGeneration > 0
31
+ && Number(activeStream?.captureGeneration || 0) === expectedCaptureGeneration
32
+ && latestFrame?.currentGenerationVerified === true
33
+ && String(latestFrame?.commandId || '') === expectedCommandId
34
+ && Number(latestFrame?.captureGeneration || 0) === expectedCaptureGeneration
35
+ && expectedMonitorIndex !== null
36
+ && activeMonitorIndex === expectedMonitorIndex
37
+ && frameMonitorIndex === expectedMonitorIndex;
38
+ }