@livedesk/hub 0.1.24 → 0.1.26

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/src/remote-hub.js CHANGED
@@ -31,6 +31,17 @@ const LIVE_STREAM_MIN_FRESH_MS = 3000;
31
31
  const LIVE_STREAM_MAX_FRESH_MS = 12000;
32
32
  const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
33
33
  const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 8000;
34
+ const TRANSPORT_DIAGNOSTIC_MIN_TTL_MS = 15_000;
35
+ const TRANSPORT_DIAGNOSTIC_MAX_TTL_MS = 180_000;
36
+ const TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS = 8_000;
37
+ const TRANSPORT_DIAGNOSTIC_RECORD_RETENTION_MS = 60_000;
38
+ const TRANSPORT_DIAGNOSTIC_MAX_RECORDS = 32;
39
+ const TRANSPORT_DIAGNOSTIC_PHASES = Object.freeze([
40
+ 'direct',
41
+ 'p2p',
42
+ 'relay',
43
+ 'recover-direct'
44
+ ]);
34
45
  const REMOTE_POLICY_BYPASS_COMMANDS = new Set([
35
46
  'ping',
36
47
  'stream.stop',
@@ -429,13 +440,23 @@ function isPublicIPv4(value) {
429
440
  return false;
430
441
  }
431
442
 
432
- if (parts[0] === 192 && (parts[1] === 0 || parts[1] === 168)) {
433
- return false;
434
- }
435
-
436
- if (parts[0] === 198 && (parts[1] === 18 || parts[1] === 19)) {
437
- return false;
438
- }
443
+ if (parts[0] === 192
444
+ && (parts[1] === 0
445
+ || parts[1] === 168
446
+ || (parts[1] === 88 && parts[2] === 99))) {
447
+ return false;
448
+ }
449
+
450
+ if (parts[0] === 198
451
+ && (parts[1] === 18
452
+ || parts[1] === 19
453
+ || (parts[1] === 51 && parts[2] === 100))) {
454
+ return false;
455
+ }
456
+
457
+ if (parts[0] === 203 && parts[1] === 0 && parts[2] === 113) {
458
+ return false;
459
+ }
439
460
 
440
461
  return true;
441
462
  }
@@ -690,21 +711,39 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
690
711
  'logs.collect'
691
712
  ]);
692
713
 
693
- function normalizeAgentOperation(value) {
694
- const operation = safeString(value, 80).toLowerCase();
695
- return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
696
- }
697
-
698
- function normalizeRemoteInputEvent(value = {}) {
699
- const input = value && typeof value === 'object' ? value : {};
700
- const type = safeString(input.type || input.Type, 48);
714
+ function normalizeAgentOperation(value) {
715
+ const operation = safeString(value, 80).toLowerCase();
716
+ return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
717
+ }
718
+
719
+ const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
720
+ 'monitorIndex',
721
+ 'MonitorIndex',
722
+ 'screenIndex',
723
+ 'ScreenIndex',
724
+ 'displayIndex',
725
+ 'DisplayIndex'
726
+ ]);
727
+
728
+ function readRawRemoteInputMonitorIndex(input) {
729
+ for (const key of REMOTE_INPUT_MONITOR_KEYS) {
730
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
731
+ return input[key];
732
+ }
733
+ }
734
+ return undefined;
735
+ }
736
+
737
+ function normalizeRemoteInputEvent(value = {}) {
738
+ const input = value && typeof value === 'object' ? value : {};
739
+ const type = safeString(input.type || input.Type, 48);
701
740
  const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
702
741
  const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
703
742
  const deltaX = Number(input.deltaX ?? input.DeltaX);
704
743
  const deltaY = Number(input.deltaY ?? input.DeltaY);
705
744
  const keyCode = Number(input.keyCode ?? input.KeyCode);
706
745
  const location = Number(input.location ?? input.Location);
707
- const monitorIndex = Number(input.monitorIndex ?? input.MonitorIndex ?? input.screenIndex ?? input.ScreenIndex ?? input.displayIndex ?? input.DisplayIndex);
746
+ const monitorIndex = parseExactLiveStreamMonitorIndex(readRawRemoteInputMonitorIndex(input));
708
747
  const inputSeq = Number(input.inputSeq ?? input.InputSeq);
709
748
  const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
710
749
  const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
@@ -720,7 +759,7 @@ function normalizeRemoteInputEvent(value = {}) {
720
759
  type,
721
760
  // A missing monitor is not equivalent to the primary display. Control
722
761
  // input must prove the exact monitor owned by its capture generation.
723
- monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : null,
762
+ monitorIndex,
724
763
  normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
725
764
  normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
726
765
  button: safeString(input.button || input.Button, 24),
@@ -1510,6 +1549,11 @@ class RemoteHubWebSocketAgentSocket {
1510
1549
 
1511
1550
  export function createRemoteHub(options = {}) {
1512
1551
  const env = options.env || process.env;
1552
+ const transportDiagnosticCommandTimeoutMs = clampNumber(
1553
+ options.transportDiagnosticCommandTimeoutMs,
1554
+ 25,
1555
+ TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS,
1556
+ TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS);
1513
1557
  const rejectFrameChannelForTest = isEnabledValue(env.LIVEDESK_TEST_MODE, false)
1514
1558
  && isEnabledValue(env.LIVEDESK_TEST_REJECT_FRAME_CHANNEL, false);
1515
1559
  const logEvent = options.logEvent || (() => {});
@@ -1613,7 +1657,9 @@ export function createRemoteHub(options = {}) {
1613
1657
  const fileSockets = new Map();
1614
1658
  const allSockets = new Set();
1615
1659
  const pendingCommandResultWaiters = new Map();
1616
- const duplicateDeviceLogAt = new Map();
1660
+ const transportDiagnostics = new Map();
1661
+ const transportDiagnosticStartingDevices = new Set();
1662
+ const duplicateDeviceLogAt = new Map();
1617
1663
  let server = null;
1618
1664
  let started = false;
1619
1665
  let boundPort = requestedPort;
@@ -2248,6 +2294,700 @@ export function createRemoteHub(options = {}) {
2248
2294
  });
2249
2295
  }
2250
2296
 
2297
+ function inspectAnnexBH264(payload) {
2298
+ if (!Buffer.isBuffer(payload) || payload.length < 5) {
2299
+ return {
2300
+ annexB: false,
2301
+ hasIdr: false,
2302
+ hasSps: false,
2303
+ hasPps: false,
2304
+ nalOrderValid: false
2305
+ };
2306
+ }
2307
+ let annexB = false;
2308
+ let hasIdr = false;
2309
+ let hasSps = false;
2310
+ let hasPps = false;
2311
+ let firstSpsAt = -1;
2312
+ let firstPpsAfterSpsAt = -1;
2313
+ let firstIdrAfterPpsAt = -1;
2314
+ for (let index = 0; index + 4 < payload.length; index += 1) {
2315
+ let headerIndex = -1;
2316
+ if (payload[index] === 0 && payload[index + 1] === 0 && payload[index + 2] === 1) {
2317
+ headerIndex = index + 3;
2318
+ } else if (payload[index] === 0
2319
+ && payload[index + 1] === 0
2320
+ && payload[index + 2] === 0
2321
+ && payload[index + 3] === 1) {
2322
+ headerIndex = index + 4;
2323
+ }
2324
+ if (headerIndex < 0 || headerIndex >= payload.length) continue;
2325
+ annexB = true;
2326
+ const nalType = payload[headerIndex] & 0x1f;
2327
+ if (nalType === 5) {
2328
+ hasIdr = true;
2329
+ if (firstPpsAfterSpsAt >= 0 && firstIdrAfterPpsAt < 0) {
2330
+ firstIdrAfterPpsAt = headerIndex;
2331
+ }
2332
+ }
2333
+ if (nalType === 7) {
2334
+ hasSps = true;
2335
+ if (firstSpsAt < 0) firstSpsAt = headerIndex;
2336
+ }
2337
+ if (nalType === 8) {
2338
+ hasPps = true;
2339
+ if (firstSpsAt >= 0 && firstPpsAfterSpsAt < 0) {
2340
+ firstPpsAfterSpsAt = headerIndex;
2341
+ }
2342
+ }
2343
+ index = headerIndex;
2344
+ }
2345
+ return {
2346
+ annexB,
2347
+ hasIdr,
2348
+ hasSps,
2349
+ hasPps,
2350
+ nalOrderValid: firstSpsAt >= 0
2351
+ && firstPpsAfterSpsAt > firstSpsAt
2352
+ && firstIdrAfterPpsAt > firstPpsAfterSpsAt
2353
+ };
2354
+ }
2355
+
2356
+ function expectedTransportForDiagnosticPhase(phase) {
2357
+ if (phase === 'p2p') return 'p2p';
2358
+ if (phase === 'relay') return 'relay';
2359
+ return 'direct';
2360
+ }
2361
+
2362
+ function observedTransportPath(frame) {
2363
+ const transport = safeString(frame?.transport, 32).toLowerCase();
2364
+ if (transport === 'udp-p2p') return 'p2p';
2365
+ if (transport === 'relay-binary') return 'relay';
2366
+ if (transport === 'binary' || transport === 'ws-binary') return 'direct';
2367
+ return '';
2368
+ }
2369
+
2370
+ function optionalFiniteNumber(value) {
2371
+ return value !== null && value !== undefined && value !== ''
2372
+ && Number.isFinite(Number(value))
2373
+ ? Number(value)
2374
+ : null;
2375
+ }
2376
+
2377
+ function transportDiagnosticIdentifierHash(value, salt) {
2378
+ const normalized = safeString(value, 240);
2379
+ return normalized
2380
+ ? crypto.createHash('sha256')
2381
+ .update(`${safeString(salt, 160)}:${normalized}`)
2382
+ .digest('hex')
2383
+ .slice(0, 16)
2384
+ : '';
2385
+ }
2386
+
2387
+ function currentTransportDiagnosticOwner(device) {
2388
+ const frame = device?.latestLiveFrame;
2389
+ if (!device?.connected
2390
+ || !device.sessionId
2391
+ || !frame
2392
+ || frame.currentGenerationVerified !== true
2393
+ || safeString(frame.mimeType, 80).toLowerCase() !== 'video/h264'
2394
+ || !safeString(frame.streamId, 128)
2395
+ || !safeString(frame.commandId, 128)
2396
+ || Number(frame.captureGeneration || 0) <= 0) {
2397
+ return null;
2398
+ }
2399
+ const monitorIndex = parseExactLiveStreamMonitorIndex(frame.monitorIndex);
2400
+ const streamState = getDeviceLiveStream(device, frame.streamId);
2401
+ if (monitorIndex === null
2402
+ || !streamState?.active
2403
+ || safeString(streamState.commandId, 128) !== safeString(frame.commandId, 128)
2404
+ || Number(streamState.captureGeneration || 0) !== Number(frame.captureGeneration || 0)
2405
+ || parseExactLiveStreamMonitorIndex(streamState.monitorIndex) !== monitorIndex) {
2406
+ return null;
2407
+ }
2408
+ return {
2409
+ deviceId: device.deviceId,
2410
+ sessionId: device.sessionId,
2411
+ streamId: safeString(frame.streamId, 128),
2412
+ streamCommandId: safeString(frame.commandId, 128),
2413
+ captureGeneration: Number(frame.captureGeneration),
2414
+ monitorIndex
2415
+ };
2416
+ }
2417
+
2418
+ function transportDiagnosticOwnersEqual(left, right) {
2419
+ return !!left
2420
+ && !!right
2421
+ && left.deviceId === right.deviceId
2422
+ && left.sessionId === right.sessionId
2423
+ && left.streamId === right.streamId
2424
+ && left.streamCommandId === right.streamCommandId
2425
+ && left.captureGeneration === right.captureGeneration
2426
+ && left.monitorIndex === right.monitorIndex;
2427
+ }
2428
+
2429
+ function transportDiagnosticOwnerMatches(record, device) {
2430
+ const current = currentTransportDiagnosticOwner(device);
2431
+ return transportDiagnosticOwnersEqual(current, record.owner);
2432
+ }
2433
+
2434
+ function serializeTransportDiagnostic(record) {
2435
+ const refreshedNetwork = udpTransport?.getDeviceDiagnosticNetwork?.(record.owner.deviceId, {
2436
+ salt: record.leaseId
2437
+ });
2438
+ if (refreshedNetwork?.evidence
2439
+ === 'authenticated-udp-endpoint-rendezvous-observed'
2440
+ || record.network?.evidence
2441
+ !== 'authenticated-udp-endpoint-rendezvous-observed') {
2442
+ record.network = refreshedNetwork || record.network;
2443
+ }
2444
+ return {
2445
+ ok: record.state !== 'failed',
2446
+ leaseId: record.leaseId,
2447
+ state: record.state,
2448
+ phase: record.phase,
2449
+ startedAt: record.startedAt,
2450
+ updatedAt: record.updatedAt,
2451
+ expiresAt: record.expiresAt,
2452
+ completedAt: record.completedAt || '',
2453
+ error: safeString(record.error, 240),
2454
+ owner: {
2455
+ deviceId: record.owner.deviceId,
2456
+ streamId: record.owner.streamId,
2457
+ streamCommandId: record.owner.streamCommandId,
2458
+ captureGeneration: record.owner.captureGeneration,
2459
+ monitorIndex: record.owner.monitorIndex
2460
+ },
2461
+ network: record.network,
2462
+ baseline: { ...record.baseline },
2463
+ frames: record.frames.map(frame => ({ ...frame })),
2464
+ cleanup: record.cleanup.map(entry => ({ ...entry }))
2465
+ };
2466
+ }
2467
+
2468
+ function getTransportDiagnostic(leaseId, token) {
2469
+ const record = transportDiagnostics.get(safeString(leaseId, 128));
2470
+ if (!record || !diagnosticSecretEquals(record.token, token)) {
2471
+ return { ok: false, error: 'transport-diagnostic-not-found' };
2472
+ }
2473
+ return serializeTransportDiagnostic(record);
2474
+ }
2475
+
2476
+ function diagnosticSecretEquals(expected, provided) {
2477
+ const expectedHash = crypto.createHash('sha256').update(String(expected || '')).digest();
2478
+ const providedHash = crypto.createHash('sha256').update(String(provided || '')).digest();
2479
+ return crypto.timingSafeEqual(expectedHash, providedHash);
2480
+ }
2481
+
2482
+ async function sendTransportDiagnosticCommand(record, operation, phase = record.phase) {
2483
+ const device = devices.get(record.owner.deviceId);
2484
+ if (!device?.connected || device.sessionId !== record.owner.sessionId) {
2485
+ return { ok: false, error: 'transport-diagnostic-device-disconnected' };
2486
+ }
2487
+ if (operation !== 'release' && !transportDiagnosticOwnerMatches(record, device)) {
2488
+ return { ok: false, error: 'transport-diagnostic-owner-changed' };
2489
+ }
2490
+ const commandId = crypto.randomUUID();
2491
+ const waiter = waitForCommandResult(device, commandId, transportDiagnosticCommandTimeoutMs);
2492
+ const queued = sendCommand(device.deviceId, {
2493
+ commandId,
2494
+ command: 'transport.diagnostic',
2495
+ payload: {
2496
+ operation,
2497
+ leaseId: record.leaseId,
2498
+ token: record.token,
2499
+ streamId: record.owner.streamId,
2500
+ streamCommandId: record.owner.streamCommandId,
2501
+ captureGeneration: record.owner.captureGeneration,
2502
+ monitorIndex: record.owner.monitorIndex,
2503
+ phase,
2504
+ ttlMs: Math.max(0, Date.parse(record.expiresAt) - Date.now())
2505
+ }
2506
+ });
2507
+ if (!queued.ok) {
2508
+ failPendingCommandResultWaiter(commandId, queued.error);
2509
+ }
2510
+ const outcome = await waiter;
2511
+ if (!outcome.ok || outcome.result?.ok === false) {
2512
+ return {
2513
+ ok: false,
2514
+ error: safeString(outcome.error || outcome.result?.error, 240)
2515
+ || 'transport-diagnostic-agent-rejected'
2516
+ };
2517
+ }
2518
+ return { ok: true };
2519
+ }
2520
+
2521
+ function appendTransportDiagnosticCleanup(record, state, reason = '') {
2522
+ record.cleanup.push({
2523
+ at: new Date().toISOString(),
2524
+ state,
2525
+ reason: safeString(reason, 160)
2526
+ });
2527
+ if (record.cleanup.length > 16) record.cleanup.splice(0, record.cleanup.length - 16);
2528
+ record.updatedAt = new Date().toISOString();
2529
+ }
2530
+
2531
+ function clearTransportDiagnosticTimers(record) {
2532
+ clearTimeout(record?.expiryTimer);
2533
+ clearTimeout(record?.removalTimer);
2534
+ if (record) {
2535
+ record.expiryTimer = null;
2536
+ record.removalTimer = null;
2537
+ }
2538
+ }
2539
+
2540
+ function removeTransportDiagnostic(record) {
2541
+ if (!record) return;
2542
+ clearTransportDiagnosticTimers(record);
2543
+ if (transportDiagnostics.get(record.leaseId) === record) {
2544
+ transportDiagnostics.delete(record.leaseId);
2545
+ }
2546
+ record.token = '';
2547
+ }
2548
+
2549
+ function scheduleTransportDiagnosticRemoval(record) {
2550
+ if (!record) return;
2551
+ clearTimeout(record.expiryTimer);
2552
+ clearTimeout(record.removalTimer);
2553
+ record.expiryTimer = null;
2554
+ record.removalTimer = setTimeout(() => {
2555
+ removeTransportDiagnostic(record);
2556
+ }, TRANSPORT_DIAGNOSTIC_RECORD_RETENTION_MS);
2557
+ record.removalTimer.unref?.();
2558
+ }
2559
+
2560
+ function pruneTransportDiagnosticRecords() {
2561
+ const finalized = [...transportDiagnostics.values()]
2562
+ .filter(record => record.state !== 'active'
2563
+ && record.state !== 'acquiring'
2564
+ && record.state !== 'expiring')
2565
+ .sort((left, right) => Date.parse(left.updatedAt || '') - Date.parse(right.updatedAt || ''));
2566
+ while (transportDiagnostics.size >= TRANSPORT_DIAGNOSTIC_MAX_RECORDS && finalized.length > 0) {
2567
+ removeTransportDiagnostic(finalized.shift());
2568
+ }
2569
+ }
2570
+
2571
+ async function expireTransportDiagnostic(record, reason = 'ttl-expired') {
2572
+ if (!record || record.state !== 'active') return;
2573
+ if (record.operationInFlight === true) {
2574
+ record.expiryTimer = setTimeout(() => {
2575
+ void expireTransportDiagnostic(record, reason);
2576
+ }, 250);
2577
+ record.expiryTimer.unref?.();
2578
+ return;
2579
+ }
2580
+ record.operationInFlight = true;
2581
+ record.state = 'expiring';
2582
+ appendTransportDiagnosticCleanup(record, 'release-requested', reason);
2583
+ try {
2584
+ const released = await sendTransportDiagnosticCommand(record, 'release');
2585
+ record.state = reason === 'ttl-expired' ? 'expired' : 'aborted';
2586
+ record.error = released.ok ? '' : released.error;
2587
+ record.completedAt = new Date().toISOString();
2588
+ appendTransportDiagnosticCleanup(
2589
+ record,
2590
+ released.ok ? 'agent-released' : 'agent-release-unconfirmed',
2591
+ released.error);
2592
+ } finally {
2593
+ record.operationInFlight = false;
2594
+ scheduleTransportDiagnosticRemoval(record);
2595
+ }
2596
+ }
2597
+
2598
+ function abortTransportDiagnosticsForDevice(device, reason) {
2599
+ for (const record of transportDiagnostics.values()) {
2600
+ if (!['active', 'acquiring', 'expiring'].includes(record.state)
2601
+ || record.owner.deviceId !== device?.deviceId
2602
+ || record.owner.sessionId !== device?.sessionId) {
2603
+ continue;
2604
+ }
2605
+ clearTimeout(record.expiryTimer);
2606
+ record.state = 'aborted';
2607
+ record.error = safeString(reason, 160) || 'device-disconnected';
2608
+ record.completedAt = new Date().toISOString();
2609
+ appendTransportDiagnosticCleanup(record, 'connection-closed', record.error);
2610
+ scheduleTransportDiagnosticRemoval(record);
2611
+ }
2612
+ }
2613
+
2614
+ async function startTransportDiagnostic(deviceId, ttlMs = 150_000, options = {}) {
2615
+ const requestSignal = options?.signal;
2616
+ const requestAborted = () => requestSignal?.aborted === true;
2617
+ const normalizedDeviceId = safeString(deviceId, 160);
2618
+ const device = devices.get(normalizedDeviceId);
2619
+ const owner = currentTransportDiagnosticOwner(device);
2620
+ if (!owner) {
2621
+ return { ok: false, error: 'transport-diagnostic-current-h264-stream-required' };
2622
+ }
2623
+ if (observedTransportPath(device.latestLiveFrame) !== 'direct'
2624
+ || safeString(device.latestLiveFrame?.frameTransportActive, 20).toLowerCase() !== 'direct') {
2625
+ return { ok: false, error: 'transport-diagnostic-direct-start-required' };
2626
+ }
2627
+ if (transportDiagnosticStartingDevices.has(normalizedDeviceId)) {
2628
+ return { ok: false, error: 'transport-diagnostic-device-lease-active' };
2629
+ }
2630
+ const existingLease = [...transportDiagnostics.values()].some(existing =>
2631
+ ['active', 'acquiring', 'expiring'].includes(existing.state)
2632
+ && existing.owner.deviceId === owner.deviceId);
2633
+ if (existingLease) {
2634
+ return { ok: false, error: 'transport-diagnostic-device-lease-active' };
2635
+ }
2636
+ transportDiagnosticStartingDevices.add(normalizedDeviceId);
2637
+ try {
2638
+ const refreshedDiagnosticNetwork =
2639
+ await udpTransport?.refreshDeviceDiagnosticNetwork?.(
2640
+ owner.deviceId,
2641
+ { timeoutMs: 3_000 });
2642
+ if (requestAborted()) {
2643
+ return { ok: false, error: 'transport-diagnostic-request-aborted' };
2644
+ }
2645
+ const physicalNetwork = udpTransport?.getDeviceDiagnosticNetwork?.(
2646
+ owner.deviceId,
2647
+ { salt: 'transport-diagnostic-preflight' }) || {};
2648
+ if (refreshedDiagnosticNetwork !== true
2649
+ || physicalNetwork.evidence
2650
+ !== 'authenticated-udp-endpoint-rendezvous-observed'
2651
+ || physicalNetwork.endpointAuthenticated !== true
2652
+ || physicalNetwork.clientAddressPublic !== true
2653
+ || physicalNetwork.hubAddressPublic !== true) {
2654
+ return {
2655
+ ok: false,
2656
+ error: 'transport-diagnostic-fresh-rendezvous-egress-required'
2657
+ };
2658
+ }
2659
+ if (physicalNetwork.differentPublicEgress !== true) {
2660
+ return {
2661
+ ok: false,
2662
+ error: 'transport-diagnostic-distinct-public-egress-required'
2663
+ };
2664
+ }
2665
+ const currentDevice = devices.get(normalizedDeviceId);
2666
+ const refreshedOwner = currentTransportDiagnosticOwner(currentDevice);
2667
+ if (!transportDiagnosticOwnersEqual(owner, refreshedOwner)
2668
+ || observedTransportPath(currentDevice?.latestLiveFrame) !== 'direct'
2669
+ || safeString(currentDevice?.latestLiveFrame?.frameTransportActive, 20).toLowerCase() !== 'direct') {
2670
+ return { ok: false, error: 'transport-diagnostic-owner-changed' };
2671
+ }
2672
+ const concurrentLease = [...transportDiagnostics.values()].some(existing =>
2673
+ ['active', 'acquiring', 'expiring'].includes(existing.state)
2674
+ && existing.owner.deviceId === owner.deviceId);
2675
+ if (concurrentLease) {
2676
+ return { ok: false, error: 'transport-diagnostic-device-lease-active' };
2677
+ }
2678
+ pruneTransportDiagnosticRecords();
2679
+ if (transportDiagnostics.size >= TRANSPORT_DIAGNOSTIC_MAX_RECORDS) {
2680
+ return { ok: false, error: 'transport-diagnostic-capacity-reached' };
2681
+ }
2682
+
2683
+ const boundedTtlMs = clampNumber(
2684
+ ttlMs,
2685
+ TRANSPORT_DIAGNOSTIC_MIN_TTL_MS,
2686
+ TRANSPORT_DIAGNOSTIC_MAX_TTL_MS,
2687
+ 150_000);
2688
+ const now = new Date();
2689
+ const leaseId = crypto.randomUUID();
2690
+ const token = crypto.randomBytes(32).toString('base64url');
2691
+ const currentFrame = currentDevice.latestLiveFrame;
2692
+ const udpStatus = udpTransport?.getDeviceStatus?.(owner.deviceId) || {};
2693
+ const hubAddressObservedAt = safeString(
2694
+ physicalNetwork.hubAddressObservedAt,
2695
+ 80);
2696
+ const record = {
2697
+ leaseId,
2698
+ token,
2699
+ owner,
2700
+ phase: 'direct',
2701
+ phaseStartedAt: '',
2702
+ state: 'acquiring',
2703
+ operationInFlight: true,
2704
+ startedAt: now.toISOString(),
2705
+ updatedAt: now.toISOString(),
2706
+ expiresAt: new Date(now.getTime() + boundedTtlMs).toISOString(),
2707
+ completedAt: '',
2708
+ error: '',
2709
+ hubAddressObservedAt,
2710
+ baseline: {
2711
+ observedAt: safeString(currentFrame.receivedAt, 80),
2712
+ frameSeq: optionalFiniteNumber(currentFrame.frameSeq),
2713
+ switchCount: optionalFiniteNumber(currentFrame.frameTransportSwitchCount),
2714
+ agentQueueDropped: optionalFiniteNumber(currentFrame.nativeOutputQueueDroppedCount),
2715
+ hubIngressDropped: optionalFiniteNumber(currentFrame.hubIngressDropped),
2716
+ udpIncompleteFrames: optionalFiniteNumber(udpStatus.droppedFrames),
2717
+ hubIngressCounterEpochHash: transportDiagnosticIdentifierHash(
2718
+ currentFrame.hubIngressCounterEpoch,
2719
+ leaseId),
2720
+ udpCounterEpochHash: transportDiagnosticIdentifierHash(
2721
+ udpStatus.sessionId,
2722
+ leaseId),
2723
+ agentQueueTelemetryAvailable:
2724
+ currentFrame.nativeOutputQueueTelemetryAvailable === true,
2725
+ hubIngressTelemetryAvailable:
2726
+ currentFrame.hubIngressTelemetryAvailable === true
2727
+ && !!currentFrame.hubIngressCounterEpoch,
2728
+ udpTelemetryAvailable:
2729
+ optionalFiniteNumber(udpStatus.droppedFrames) !== null
2730
+ && !!udpStatus.sessionId
2731
+ },
2732
+ frames: [],
2733
+ cleanup: [],
2734
+ network: udpTransport?.getDeviceDiagnosticNetwork?.(owner.deviceId, {
2735
+ salt: leaseId
2736
+ }) || {
2737
+ evidence: 'unavailable',
2738
+ differentPublicEgress: null,
2739
+ hubAddressObservedAt
2740
+ },
2741
+ expiryTimer: null,
2742
+ removalTimer: null
2743
+ };
2744
+ transportDiagnostics.set(leaseId, record);
2745
+ const acquired = await sendTransportDiagnosticCommand(record, 'acquire', 'direct');
2746
+ record.operationInFlight = false;
2747
+ if (!acquired.ok || record.state !== 'acquiring') {
2748
+ const acquireError = acquired.ok
2749
+ ? safeString(record.error, 240) || 'transport-diagnostic-acquire-interrupted'
2750
+ : acquired.error;
2751
+ record.error = acquireError;
2752
+ record.completedAt = new Date().toISOString();
2753
+ appendTransportDiagnosticCleanup(record, 'acquire-failed', record.error);
2754
+ appendTransportDiagnosticCleanup(
2755
+ record,
2756
+ 'release-requested',
2757
+ 'acquire-outcome-uncertain');
2758
+ record.operationInFlight = true;
2759
+ let released;
2760
+ try {
2761
+ released = await sendTransportDiagnosticCommand(record, 'release');
2762
+ } catch (error) {
2763
+ released = {
2764
+ ok: false,
2765
+ error: safeString(error instanceof Error ? error.message : error, 240)
2766
+ || 'transport-diagnostic-agent-release-failed'
2767
+ };
2768
+ } finally {
2769
+ record.operationInFlight = false;
2770
+ }
2771
+ appendTransportDiagnosticCleanup(
2772
+ record,
2773
+ released.ok ? 'agent-released' : 'agent-release-unconfirmed',
2774
+ released.error);
2775
+ record.state = 'failed';
2776
+ record.completedAt = new Date().toISOString();
2777
+ const failed = { ...serializeTransportDiagnostic(record), token: '' };
2778
+ removeTransportDiagnostic(record);
2779
+ return failed;
2780
+ }
2781
+ record.state = 'active';
2782
+ record.phaseStartedAt = new Date().toISOString();
2783
+ record.updatedAt = record.phaseStartedAt;
2784
+ appendTransportDiagnosticCleanup(record, 'lease-acquired');
2785
+ if (requestAborted()) {
2786
+ await expireTransportDiagnostic(record, 'start-request-aborted');
2787
+ return { ok: false, error: 'transport-diagnostic-request-aborted' };
2788
+ }
2789
+ const remainingTtlMs = Math.max(1, Date.parse(record.expiresAt) - Date.now());
2790
+ record.expiryTimer = setTimeout(() => {
2791
+ void expireTransportDiagnostic(record, 'ttl-expired');
2792
+ }, remainingTtlMs);
2793
+ record.expiryTimer.unref?.();
2794
+ return { ...serializeTransportDiagnostic(record), token };
2795
+ } finally {
2796
+ transportDiagnosticStartingDevices.delete(normalizedDeviceId);
2797
+ }
2798
+ }
2799
+
2800
+ async function setTransportDiagnosticPhase(leaseId, token, phase) {
2801
+ const record = transportDiagnostics.get(safeString(leaseId, 128));
2802
+ const normalizedPhase = safeString(phase, 32).toLowerCase();
2803
+ if (!record || !diagnosticSecretEquals(record.token, token)) {
2804
+ return { ok: false, error: 'transport-diagnostic-not-found' };
2805
+ }
2806
+ if (record.state !== 'active') {
2807
+ return { ok: false, error: 'transport-diagnostic-not-active' };
2808
+ }
2809
+ if (record.operationInFlight === true) {
2810
+ return { ok: false, error: 'transport-diagnostic-operation-in-flight' };
2811
+ }
2812
+ if (!TRANSPORT_DIAGNOSTIC_PHASES.includes(normalizedPhase)) {
2813
+ return { ok: false, error: 'transport-diagnostic-phase-invalid' };
2814
+ }
2815
+ const currentIndex = TRANSPORT_DIAGNOSTIC_PHASES.indexOf(record.phase);
2816
+ const nextIndex = TRANSPORT_DIAGNOSTIC_PHASES.indexOf(normalizedPhase);
2817
+ const currentObserved = record.frames.some(frame => frame.phase === record.phase && frame.accepted === true);
2818
+ if (nextIndex !== currentIndex + 1 || !currentObserved) {
2819
+ return { ok: false, error: 'transport-diagnostic-phase-order-required' };
2820
+ }
2821
+ record.operationInFlight = true;
2822
+ try {
2823
+ const updated = await sendTransportDiagnosticCommand(record, 'set-phase', normalizedPhase);
2824
+ if (!updated.ok) {
2825
+ record.error = updated.error;
2826
+ record.updatedAt = new Date().toISOString();
2827
+ return { ok: false, error: updated.error };
2828
+ }
2829
+ if (record.state !== 'active') {
2830
+ return { ok: false, error: 'transport-diagnostic-interrupted' };
2831
+ }
2832
+ record.phase = normalizedPhase;
2833
+ record.phaseStartedAt = new Date().toISOString();
2834
+ record.updatedAt = record.phaseStartedAt;
2835
+ return serializeTransportDiagnostic(record);
2836
+ } finally {
2837
+ record.operationInFlight = false;
2838
+ }
2839
+ }
2840
+
2841
+ async function stopTransportDiagnostic(leaseId, token, reason = 'runner-complete') {
2842
+ const record = transportDiagnostics.get(safeString(leaseId, 128));
2843
+ if (!record || !diagnosticSecretEquals(record.token, token)) {
2844
+ return { ok: false, error: 'transport-diagnostic-not-found' };
2845
+ }
2846
+ if (record.state !== 'active') return serializeTransportDiagnostic(record);
2847
+ if (record.operationInFlight === true) {
2848
+ return { ok: false, error: 'transport-diagnostic-operation-in-flight' };
2849
+ }
2850
+ clearTimeout(record.expiryTimer);
2851
+ record.operationInFlight = true;
2852
+ try {
2853
+ appendTransportDiagnosticCleanup(record, 'release-requested', reason);
2854
+ const released = await sendTransportDiagnosticCommand(record, 'release');
2855
+ record.state = released.ok ? 'completed' : 'failed';
2856
+ record.error = released.ok ? '' : released.error;
2857
+ record.completedAt = new Date().toISOString();
2858
+ appendTransportDiagnosticCleanup(
2859
+ record,
2860
+ released.ok ? 'agent-released' : 'agent-release-unconfirmed',
2861
+ released.error);
2862
+ const serialized = serializeTransportDiagnostic(record);
2863
+ scheduleTransportDiagnosticRemoval(record);
2864
+ return serialized;
2865
+ } finally {
2866
+ record.operationInFlight = false;
2867
+ }
2868
+ }
2869
+
2870
+ function recordTransportDiagnosticFrame(device, message, payload, frame) {
2871
+ if (transportDiagnostics.size === 0 || message.transportDiagnosticApplied !== true) {
2872
+ return;
2873
+ }
2874
+ const leaseId = safeString(message.transportDiagnosticLeaseId, 128);
2875
+ const phase = safeString(message.transportDiagnosticPhase, 32).toLowerCase();
2876
+ if (!leaseId) return;
2877
+ const record = transportDiagnostics.get(leaseId);
2878
+ if (!record
2879
+ || record.state !== 'active'
2880
+ || record.phase !== phase
2881
+ || !transportDiagnosticOwnerMatches(record, device)
2882
+ || frame.streamId !== record.owner.streamId
2883
+ || frame.commandId !== record.owner.streamCommandId
2884
+ || frame.captureGeneration !== record.owner.captureGeneration
2885
+ || frame.monitorIndex !== record.owner.monitorIndex
2886
+ || safeString(frame.mimeType, 80).toLowerCase() !== 'video/h264') {
2887
+ return;
2888
+ }
2889
+ const expectedPath = expectedTransportForDiagnosticPhase(phase);
2890
+ const h264 = inspectAnnexBH264(payload);
2891
+ const agentPath = safeString(frame.frameTransportActive, 20).toLowerCase();
2892
+ const hubObservedPath = observedTransportPath(frame);
2893
+ const frameSeq = optionalFiniteNumber(frame.frameSeq);
2894
+ const switchCount = optionalFiniteNumber(message.frameTransportSwitchCount);
2895
+ const lastAcceptedFrame = [...record.frames]
2896
+ .reverse()
2897
+ .find(item => item.accepted === true);
2898
+ const previousFrameSeq = optionalFiniteNumber(
2899
+ lastAcceptedFrame?.frameSeq ?? record.baseline?.frameSeq);
2900
+ const previousSwitchCount = optionalFiniteNumber(
2901
+ lastAcceptedFrame?.switchCount ?? record.baseline?.switchCount);
2902
+ const sequenceAdvanced = frameSeq !== null
2903
+ && (previousFrameSeq === null || frameSeq > previousFrameSeq);
2904
+ const switchAdvanced = switchCount !== null
2905
+ && (lastAcceptedFrame
2906
+ ? previousSwitchCount !== null && switchCount > previousSwitchCount
2907
+ : previousSwitchCount === null || switchCount >= previousSwitchCount);
2908
+ const agentQueueDepth = optionalFiniteNumber(message.nativeOutputQueueDepth);
2909
+ const agentQueueBytes = optionalFiniteNumber(message.nativeOutputQueueBytes);
2910
+ const agentQueueDropped = optionalFiniteNumber(message.nativeOutputQueueDroppedCount);
2911
+ const hubIngressDropped = optionalFiniteNumber(message.hubIngressDropped);
2912
+ const udpIncompleteFrames = optionalFiniteNumber(message.udpIncompleteFrames);
2913
+ const hubIngressCounterEpochHash = transportDiagnosticIdentifierHash(
2914
+ message.hubIngressCounterEpoch,
2915
+ record.leaseId);
2916
+ const agentQueueTelemetryAvailable = agentQueueDepth !== null
2917
+ && agentQueueBytes !== null
2918
+ && agentQueueDropped !== null;
2919
+ const hubIngressTelemetryAvailable = hubIngressDropped !== null
2920
+ && !!hubIngressCounterEpochHash;
2921
+ const udpTelemetryAvailable = phase !== 'p2p' || udpIncompleteFrames !== null;
2922
+ const phaseLatencyMs = Math.max(
2923
+ 0,
2924
+ Date.parse(frame.receivedAt || '') - Date.parse(record.phaseStartedAt || ''));
2925
+ const accepted = agentPath === expectedPath
2926
+ && hubObservedPath === expectedPath
2927
+ && frame.isKeyFrame === true
2928
+ && h264.annexB
2929
+ && h264.hasIdr
2930
+ && h264.hasSps
2931
+ && h264.hasPps
2932
+ && h264.nalOrderValid
2933
+ && sequenceAdvanced
2934
+ && switchAdvanced
2935
+ && agentQueueTelemetryAvailable
2936
+ && hubIngressTelemetryAvailable
2937
+ && udpTelemetryAvailable
2938
+ && Number.isFinite(phaseLatencyMs);
2939
+ if (record.frames.some(item => item.phase === phase && item.accepted === true)) return;
2940
+ const observation = {
2941
+ phase,
2942
+ expectedPath,
2943
+ actualPath: agentPath,
2944
+ hubObservedPath,
2945
+ protocol: safeString(frame.frameTransportProtocol, 20),
2946
+ transport: safeString(frame.transport, 32),
2947
+ reason: safeString(frame.frameTransportReason, 160),
2948
+ receivedAt: frame.receivedAt,
2949
+ frameSeq,
2950
+ byteLength: frame.byteLength,
2951
+ contentHash: safeString(frame.contentHash, 128),
2952
+ isKeyFrame: frame.isKeyFrame === true,
2953
+ annexB: h264.annexB,
2954
+ hasIdr: h264.hasIdr,
2955
+ hasSps: h264.hasSps,
2956
+ hasPps: h264.hasPps,
2957
+ nalOrderValid: h264.nalOrderValid,
2958
+ switchCount,
2959
+ tcpFailureCount: frame.frameTransportTcpFailureCount,
2960
+ phaseLatencyMs,
2961
+ sequenceAdvanced,
2962
+ switchAdvanced,
2963
+ agentQueueDepth,
2964
+ agentQueueBytes,
2965
+ agentQueueDropped,
2966
+ hubIngressDropped,
2967
+ udpIncompleteFrames,
2968
+ hubIngressCounterEpochHash,
2969
+ agentQueueTelemetryAvailable,
2970
+ hubIngressTelemetryAvailable,
2971
+ udpTelemetryAvailable,
2972
+ accepted
2973
+ };
2974
+ if (accepted) {
2975
+ record.frames = record.frames.filter(item => item.phase !== phase);
2976
+ record.frames.push(observation);
2977
+ } else {
2978
+ const rejectedIndex = record.frames.findIndex(item =>
2979
+ item.phase === phase && item.accepted !== true);
2980
+ if (rejectedIndex >= 0) record.frames[rejectedIndex] = observation;
2981
+ else record.frames.push(observation);
2982
+ }
2983
+ if (record.frames.length > TRANSPORT_DIAGNOSTIC_PHASES.length * 2) {
2984
+ record.frames = record.frames.filter(item => item.accepted === true)
2985
+ .concat(record.frames.filter(item => item.accepted !== true)
2986
+ .slice(-TRANSPORT_DIAGNOSTIC_PHASES.length));
2987
+ }
2988
+ record.updatedAt = new Date().toISOString();
2989
+ }
2990
+
2251
2991
  function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
2252
2992
  const now = new Date().toISOString();
2253
2993
  const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
@@ -3117,9 +3857,10 @@ export function createRemoteHub(options = {}) {
3117
3857
  return;
3118
3858
  }
3119
3859
 
3120
- device.connected = false;
3121
- device.socket = null;
3122
- udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
3860
+ device.connected = false;
3861
+ device.socket = null;
3862
+ abortTransportDiagnosticsForDevice(device, reason);
3863
+ udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
3123
3864
  closeInputSocket(device, reason);
3124
3865
  closeFrameSocket(device, reason);
3125
3866
  device.disconnectedAt = new Date().toISOString();
@@ -3560,6 +4301,48 @@ export function createRemoteHub(options = {}) {
3560
4301
  ?? result.HubForwardedAtEpochMs
3561
4302
  ?? pending?.hubForwardedAtEpochMs
3562
4303
  ?? 0) || 0,
4304
+ duplicate: message.duplicate === true
4305
+ || message.Duplicate === true
4306
+ || result.duplicate === true
4307
+ || result.Duplicate === true,
4308
+ nativeInputBackend: safeString(
4309
+ message.nativeInputBackend
4310
+ || message.NativeInputBackend
4311
+ || result.nativeInputBackend
4312
+ || result.NativeInputBackend,
4313
+ 80),
4314
+ nativeEventsRequested: Math.max(0, Number(
4315
+ message.nativeEventsRequested
4316
+ ?? message.NativeEventsRequested
4317
+ ?? result.nativeEventsRequested
4318
+ ?? result.NativeEventsRequested
4319
+ ?? 0) || 0),
4320
+ nativeEventsAccepted: Math.max(0, Number(
4321
+ message.nativeEventsAccepted
4322
+ ?? message.NativeEventsAccepted
4323
+ ?? result.nativeEventsAccepted
4324
+ ?? result.NativeEventsAccepted
4325
+ ?? 0) || 0),
4326
+ hookEventsObserved: Math.max(0, Number(
4327
+ message.hookEventsObserved
4328
+ ?? message.HookEventsObserved
4329
+ ?? result.hookEventsObserved
4330
+ ?? result.HookEventsObserved
4331
+ ?? 0) || 0),
4332
+ hookEventsBlocked: Math.max(0, Number(
4333
+ message.hookEventsBlocked
4334
+ ?? message.HookEventsBlocked
4335
+ ?? result.hookEventsBlocked
4336
+ ?? result.HookEventsBlocked
4337
+ ?? 0) || 0),
4338
+ focusChanged: message.focusChanged === true
4339
+ || message.FocusChanged === true
4340
+ || result.focusChanged === true
4341
+ || result.FocusChanged === true,
4342
+ inputDiagnostic: message.inputDiagnostic === true
4343
+ || message.InputDiagnostic === true
4344
+ || result.inputDiagnostic === true
4345
+ || result.InputDiagnostic === true,
3563
4346
  fallback: pending?.fallback === true,
3564
4347
  hubAcknowledgedAtEpochMs: Date.now()
3565
4348
  };
@@ -4408,10 +5191,17 @@ export function createRemoteHub(options = {}) {
4408
5191
  nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
4409
5192
  nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
4410
5193
  nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
5194
+ nativeOutputQueueTelemetryAvailable:
5195
+ optionalFiniteNumber(message.nativeOutputQueueDepth) !== null
5196
+ && optionalFiniteNumber(message.nativeOutputQueueBytes) !== null
5197
+ && optionalFiniteNumber(message.nativeOutputQueueDroppedCount) !== null,
4411
5198
  nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
4412
5199
  droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
4413
5200
  hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
4414
5201
  udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
5202
+ hubIngressTelemetryAvailable: optionalFiniteNumber(message.hubIngressDropped) !== null,
5203
+ udpIncompleteTelemetryAvailable: optionalFiniteNumber(message.udpIncompleteFrames) !== null,
5204
+ hubIngressCounterEpoch: safeString(message.hubIngressCounterEpoch, 160),
4415
5205
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
4416
5206
  platformProfile: safeString(message.platformProfile, 80),
4417
5207
  monitorIndex: activeMonitorIndex,
@@ -4462,9 +5252,10 @@ export function createRemoteHub(options = {}) {
4462
5252
  frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
4463
5253
  sameContentStreak,
4464
5254
  payload,
4465
- accessToken: createFrameAccessToken()
4466
- };
4467
- streamState.latestFrame = device.latestLiveFrame;
5255
+ accessToken: createFrameAccessToken()
5256
+ };
5257
+ recordTransportDiagnosticFrame(device, message, payload, device.latestLiveFrame);
5258
+ streamState.latestFrame = device.latestLiveFrame;
4468
5259
  rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
4469
5260
  emitFrame({
4470
5261
  kind: 'live',
@@ -4659,7 +5450,8 @@ export function createRemoteHub(options = {}) {
4659
5450
  if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
4660
5451
  applyLiveFrame(device, {
4661
5452
  ...header,
4662
- hubIngressDropped: Number(state.binaryQueueDrops || 0)
5453
+ hubIngressDropped: Number(state.binaryQueueDrops || 0),
5454
+ hubIngressCounterEpoch: state.binaryQueueEpoch
4663
5455
  }, framePayload, binaryTransport);
4664
5456
  return;
4665
5457
  }
@@ -4700,10 +5492,11 @@ export function createRemoteHub(options = {}) {
4700
5492
  lastFrameAt: new Date().toISOString(),
4701
5493
  framesReceived: Number(device.udp?.framesReceived || 0) + 1
4702
5494
  };
4703
- return applyLiveFrame(device, {
4704
- ...header,
4705
- transport: 'udp-p2p'
4706
- }, payload, 'udp-p2p');
5495
+ return applyLiveFrame(device, {
5496
+ ...header,
5497
+ transport: 'udp-p2p',
5498
+ hubIngressCounterEpoch: sessionId
5499
+ }, payload, 'udp-p2p');
4707
5500
  }
4708
5501
 
4709
5502
  function dropQueuedAgentBinaryFrame(state) {
@@ -5033,9 +5826,10 @@ export function createRemoteHub(options = {}) {
5033
5826
  binaryQueue: [],
5034
5827
  binaryQueueDraining: false,
5035
5828
  binaryQueueScheduled: false,
5036
- binaryQueueSocket: null,
5037
- binaryQueueDrops: 0,
5038
- closed: false
5829
+ binaryQueueSocket: null,
5830
+ binaryQueueDrops: 0,
5831
+ binaryQueueEpoch: crypto.randomUUID(),
5832
+ closed: false
5039
5833
  };
5040
5834
 
5041
5835
  const helloTimer = setTimeout(() => {
@@ -5193,9 +5987,10 @@ export function createRemoteHub(options = {}) {
5193
5987
  binaryQueue: [],
5194
5988
  binaryQueueDraining: false,
5195
5989
  binaryQueueScheduled: false,
5196
- binaryQueueSocket: null,
5197
- binaryQueueDrops: 0,
5198
- closed: false
5990
+ binaryQueueSocket: null,
5991
+ binaryQueueDrops: 0,
5992
+ binaryQueueEpoch: crypto.randomUUID(),
5993
+ closed: false
5199
5994
  };
5200
5995
 
5201
5996
  const helloTimer = setTimeout(() => {
@@ -5419,8 +6214,9 @@ export function createRemoteHub(options = {}) {
5419
6214
  return getStatus({ includeSecrets: false });
5420
6215
  }
5421
6216
 
5422
- async function close() {
6217
+ async function close() {
5423
6218
  for (const device of devices.values()) {
6219
+ abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
5424
6220
  failAllPendingTasks(device, 'hub-shutdown');
5425
6221
  failAllPendingInputFallbacks(device, 'hub-shutdown');
5426
6222
  failPendingCommandResultWaiters(device, 'hub-shutdown');
@@ -5472,10 +6268,11 @@ export function createRemoteHub(options = {}) {
5472
6268
 
5473
6269
  function disconnectDevice(deviceId, reason = 'manager-disconnect') {
5474
6270
  const device = devices.get(String(deviceId || ''));
5475
- if (device?.synthetic === true) {
5476
- device.connected = false;
5477
- device.disconnectedAt = new Date().toISOString();
5478
- device.lastDisconnectReason = reason;
6271
+ if (device?.synthetic === true) {
6272
+ device.connected = false;
6273
+ device.disconnectedAt = new Date().toISOString();
6274
+ device.lastDisconnectReason = reason;
6275
+ abortTransportDiagnosticsForDevice(device, reason);
5479
6276
  deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
5480
6277
  failAllPendingTasks(device, 'device-disconnected');
5481
6278
  failAllPendingInputFallbacks(device, 'device-disconnected');
@@ -7529,6 +8326,10 @@ export function createRemoteHub(options = {}) {
7529
8326
  stopLiveStream,
7530
8327
  pauseLiveCapture,
7531
8328
  resumeLiveCapture,
8329
+ startTransportDiagnostic,
8330
+ getTransportDiagnostic,
8331
+ setTransportDiagnosticPhase,
8332
+ stopTransportDiagnostic,
7532
8333
  startAudioStream,
7533
8334
  stopAudioStream,
7534
8335
  getDeviceLiveFrame,