livedesk 0.1.448 → 0.1.450

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.
@@ -39,6 +39,9 @@ const DEFAULT_RELAY_PORT = 5199;
39
39
  const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
40
40
  const DEFAULT_AUTH_CALLBACK_PORT = 5179;
41
41
  const ENDPOINT_PROBE_TIMEOUT_MS = 900;
42
+ const FRESH_REGISTRY_TIMEOUT_MS = 4000;
43
+ const FRESH_REGISTRY_TIMEOUT_MIN_MS = 25;
44
+ const FRESH_REGISTRY_TIMEOUT_MAX_MS = 10000;
42
45
  // Hub-online delivery is an optimization, not a correctness dependency. A
43
46
  // browser, proxy, or suspended network can miss the wake event, so registry
44
47
  // polling must stay interactive instead of backing off to multi-minute waits.
@@ -49,6 +52,7 @@ const EXIT_INVALID_PAIR_TOKEN = 23;
49
52
  const EXIT_CLIENT_UPDATE = 42;
50
53
  const SESSION_REFRESH_SKEW_SECONDS = 60;
51
54
  const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
55
+ const HUB_TARGET_CACHE_FUTURE_SKEW_MS = 5 * 60 * 1000;
52
56
  const WINDOWS_STARTUP_SCRIPT_NAME = 'LiveDesk Desktop.vbs';
53
57
  const WINDOWS_STARTUP_LEGACY_SCRIPT_NAME = 'LiveDesk Client.vbs';
54
58
  const WINDOWS_STARTUP_LEGACY_CMD_NAME = 'LiveDesk Client.cmd';
@@ -67,6 +71,11 @@ const FAST_PREFLIGHT_CACHE_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'fast-preflight
67
71
  const CLIENT_SLOT_PATH = join(UNIFIED_CLIENT_STATE_DIR, 'device-slot.json');
68
72
  const CLIENT_UPDATE_STATE_PATH = process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH
69
73
  || join(UNIFIED_CLIENT_STATE_DIR, 'client-update.json');
74
+ // Migration-only compatibility: releases before 0.1.204 could persist these
75
+ // Client-side model options in startup/update commands. Consume them without
76
+ // forwarding so an upgrade starts normally, but never restores that feature.
77
+ const RETIRED_CLIENT_MODEL_SINGLE_OPTIONS = new Set(['--ai', '--no-ai', '--fake-ai']);
78
+ const RETIRED_CLIENT_MODEL_VALUE_OPTION = '--ai-model';
70
79
  let activeAgentProcess = null;
71
80
  let roleRestartRequest = null;
72
81
  let agentRestartRequest = null;
@@ -301,8 +310,8 @@ Options:
301
310
  --version Show the agent version.
302
311
  --help Show this help.
303
312
 
304
- Auto uses C# RemoteFast when supported and falls back to Node for AI assist or
305
- when a packaged RemoteFast runtime is unavailable.
313
+ Auto uses C# RemoteFast when supported and falls back to Node only when a
314
+ packaged RemoteFast runtime is unavailable.
306
315
  Enable Windows auto-start from the connection page when this client signs in.
307
316
  `.trimStart());
308
317
  }
@@ -394,15 +403,30 @@ export function parseLauncherArgs(argv) {
394
403
  let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
395
404
  let authPort = normalizePort(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_CLIENT_AUTH_PORT) || DEFAULT_AUTH_CALLBACK_PORT;
396
405
  let command = 'connect';
397
- let nodeOnlyFeature = false;
398
- let fakeThumbnail = false;
406
+ let fakeThumbnail = false;
399
407
  let startupRun = isTruthy(process.env.LIVEDESK_ROLE_TRANSITION);
400
- let openBrowserOnStart = !isTruthy(process.env.LIVEDESK_SKIP_BROWSER_OPEN);
401
- let updateWaitPid = 0;
402
-
403
- for (let index = 0; index < argv.length; index += 1) {
404
- const arg = argv[index];
405
- if (arg && !arg.startsWith('-')) {
408
+ let openBrowserOnStart = !isTruthy(process.env.LIVEDESK_SKIP_BROWSER_OPEN);
409
+ let updateWaitPid = 0;
410
+ let retiredModelOptionsDiscarded = 0;
411
+
412
+ for (let index = 0; index < argv.length; index += 1) {
413
+ const arg = argv[index];
414
+ if (RETIRED_CLIENT_MODEL_SINGLE_OPTIONS.has(arg)) {
415
+ retiredModelOptionsDiscarded += 1;
416
+ continue;
417
+ }
418
+ if (arg === RETIRED_CLIENT_MODEL_VALUE_OPTION) {
419
+ retiredModelOptionsDiscarded += 1;
420
+ if (index + 1 < argv.length && !String(argv[index + 1] || '').startsWith('-')) {
421
+ index += 1;
422
+ }
423
+ continue;
424
+ }
425
+ if (String(arg || '').startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
426
+ retiredModelOptionsDiscarded += 1;
427
+ continue;
428
+ }
429
+ if (arg && !arg.startsWith('-')) {
406
430
  const lowerArg = arg.toLowerCase();
407
431
  const shortcutSlot = normalizeSlotNumber(arg);
408
432
  if (lowerArg === 'connect') {
@@ -512,11 +536,8 @@ export function parseLauncherArgs(argv) {
512
536
  if (arg === '--version') {
513
537
  version = true;
514
538
  }
515
- if (arg === '--ai' || arg === '--fake-ai' || arg === '--ai-model' || arg === '--no-ai') {
516
- nodeOnlyFeature = true;
517
- }
518
- if (arg === '--fake-thumbnail') {
519
- fakeThumbnail = true;
539
+ if (arg === '--fake-thumbnail') {
540
+ fakeThumbnail = true;
520
541
  }
521
542
  forwarded.push(arg);
522
543
  }
@@ -539,13 +560,13 @@ export function parseLauncherArgs(argv) {
539
560
  deviceId,
540
561
  slot,
541
562
  authPort,
542
- nodeOnlyFeature,
543
- fakeThumbnail,
544
- startupRun,
545
- openBrowserOnStart,
546
- updateWaitPid
547
- };
548
- }
563
+ fakeThumbnail,
564
+ startupRun,
565
+ openBrowserOnStart,
566
+ updateWaitPid,
567
+ retiredModelOptionsDiscarded
568
+ };
569
+ }
549
570
 
550
571
  function normalizePort(value) {
551
572
  const port = Number(String(value || '').trim());
@@ -854,13 +875,15 @@ const CLIENT_UPDATE_SINGLE_FLAGS = new Set([
854
875
  '--no-open',
855
876
  '--exit-on-disconnect',
856
877
  '--exit-on-invalid-pair',
857
- '--once'
878
+ '--once',
879
+ ...RETIRED_CLIENT_MODEL_SINGLE_OPTIONS
858
880
  ]);
859
881
  const CLIENT_UPDATE_VALUE_FLAGS = new Set([
860
882
  '--update-wait-pid',
861
883
  // Pairing credentials stay in the inherited private environment and must
862
884
  // never be copied onto a replacement process command line.
863
- '--pair'
885
+ '--pair',
886
+ RETIRED_CLIENT_MODEL_VALUE_OPTION
864
887
  ]);
865
888
 
866
889
  export function buildClientUpdateRestartArgs(prepared = {}) {
@@ -874,6 +897,9 @@ export function buildClientUpdateRestartArgs(prepared = {}) {
874
897
  if (arg.startsWith('--pair=')) {
875
898
  continue;
876
899
  }
900
+ if (arg.startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
901
+ continue;
902
+ }
877
903
  if (CLIENT_UPDATE_VALUE_FLAGS.has(arg)) {
878
904
  index += 1;
879
905
  continue;
@@ -945,7 +971,9 @@ export function normalizeCachedHubTarget(value, expectedOwnerKey, now = Date.now
945
971
  return null;
946
972
  }
947
973
  const updatedAt = Date.parse(String(cached.updatedAt || ''));
948
- if (!Number.isFinite(updatedAt) || now - updatedAt > HUB_TARGET_CACHE_MAX_AGE_MS) {
974
+ if (!Number.isFinite(updatedAt)
975
+ || updatedAt > now + HUB_TARGET_CACHE_FUTURE_SKEW_MS
976
+ || now - updatedAt > HUB_TARGET_CACHE_MAX_AGE_MS) {
949
977
  return null;
950
978
  }
951
979
  const pair = String(cached.pair || '').trim();
@@ -1109,13 +1137,63 @@ function isTransientNetworkError(error) {
1109
1137
  return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
1110
1138
  }
1111
1139
 
1112
- function formatDiscoveryError(error) {
1113
- if (isTransientNetworkError(error)) {
1114
- const code = getNestedErrorCode(error);
1115
- return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
1116
- }
1117
- return error instanceof Error ? error.message : String(error);
1118
- }
1140
+ function formatDiscoveryError(error) {
1141
+ if (isTransientNetworkError(error)) {
1142
+ const code = getNestedErrorCode(error);
1143
+ return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
1144
+ }
1145
+ return error instanceof Error ? error.message : String(error);
1146
+ }
1147
+
1148
+ function createHubDiscoveryError(code, message, cause = null) {
1149
+ const error = new Error(message);
1150
+ error.code = String(code || '').trim();
1151
+ if (cause) {
1152
+ error.cause = cause;
1153
+ }
1154
+ return error;
1155
+ }
1156
+
1157
+ export function canUseCachedRelayAfterRegistryError(error) {
1158
+ const code = getNestedErrorCode(error).toLowerCase();
1159
+ if ([
1160
+ 'hub-registry-missing',
1161
+ 'hub-registry-expired',
1162
+ 'hub-registry-timeout',
1163
+ 'pin-not-found',
1164
+ 'pin-expired',
1165
+ 'resolver-unavailable'
1166
+ ].includes(code)) {
1167
+ return true;
1168
+ }
1169
+ // Semantic registry failures must never be reclassified as network errors
1170
+ // merely because their message mentions a "connection token".
1171
+ if (code.startsWith('hub-registry-')
1172
+ || code.startsWith('pin-registry-')
1173
+ || ['invalid-pin', 'rate-limited', 'request-too-large'].includes(code)) {
1174
+ return false;
1175
+ }
1176
+ return isTransientNetworkError(error);
1177
+ }
1178
+
1179
+ async function normalizePinResolverInvocationError(error) {
1180
+ if (isTransientNetworkError(error)) {
1181
+ return error;
1182
+ }
1183
+ const response = error?.context;
1184
+ const status = Number(response?.status || 0);
1185
+ let reason = '';
1186
+ try {
1187
+ const payload = await response?.clone?.().json?.();
1188
+ reason = String(payload?.reason || '').trim().toLowerCase();
1189
+ } catch {
1190
+ // HTTP status still gives a conservative semantic classification below.
1191
+ }
1192
+ if (!reason && status === 404) reason = 'pin-not-found';
1193
+ if (!reason && status === 503) reason = 'resolver-unavailable';
1194
+ if (!reason) return error;
1195
+ return createHubDiscoveryError(reason, reason, error);
1196
+ }
1119
1197
 
1120
1198
  async function recoverRotatedSession(supabase, previousSession) {
1121
1199
  for (const delayMs of [100, 250, 500]) {
@@ -2618,54 +2696,72 @@ function readRequestBody(req, maxBytes = 4096) {
2618
2696
  });
2619
2697
  }
2620
2698
 
2621
- async function resolveManagerFromPin(supabase, pin, options = {}) {
2699
+ export async function resolveManagerFromPin(supabase, pin, options = {}) {
2622
2700
  const normalizedPin = normalizePairingPin(pin);
2623
2701
  if (!normalizedPin) {
2624
2702
  throw new Error('Enter a 6-digit LiveDesk PIN.');
2625
2703
  }
2626
2704
  const cacheOwnerKey = hubCacheOwnerForPin(normalizedPin);
2627
- const cachedTarget = await resolveManagerFromCachedTarget(cacheOwnerKey);
2628
- if (cachedTarget) {
2629
- console.log(`Found cached LiveDesk Hub at ${cachedTarget.manager}.`);
2630
- return cachedTarget;
2631
- }
2632
-
2633
- const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
2634
- body: { pin: normalizedPin }
2635
- });
2636
- if (error) {
2637
- throw error;
2638
- }
2639
- const result = Array.isArray(data) ? data[0] : data;
2640
- if (!result?.ok) {
2641
- throw new Error(result?.reason || 'pin-not-found');
2642
- }
2643
- if (!result.pair_token) {
2644
- throw new Error('The PIN matched a Hub record without a private connection token.');
2645
- }
2646
- const endpointCandidates = normalizeEndpointCandidates(result);
2647
- if (endpointCandidates.length === 0) {
2648
- throw new Error('The PIN matched a Hub record without a reachable address.');
2649
- }
2650
- const target = await chooseManagerConnectionTarget(endpointCandidates, {
2651
- allowRelayFallback: options.allowRelayFallback === true
2705
+ const resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
2706
+ allowRelayFallback: options.allowRelayFallback === true,
2707
+ relayEndpoint: options.relayEndpoint,
2708
+ probeEndpoint: options.probeEndpoint,
2709
+ resolveFreshTarget: async () => {
2710
+ const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
2711
+ body: { pin: normalizedPin }
2712
+ });
2713
+ if (error) {
2714
+ throw await normalizePinResolverInvocationError(error);
2715
+ }
2716
+ const result = Array.isArray(data) ? data[0] : data;
2717
+ if (!result?.ok) {
2718
+ const reason = String(result?.reason || 'pin-not-found').trim().toLowerCase();
2719
+ throw createHubDiscoveryError(reason, reason);
2720
+ }
2721
+ if (!result.pair_token) {
2722
+ throw createHubDiscoveryError(
2723
+ 'pin-registry-pair-token-missing',
2724
+ 'The PIN matched a Hub record without a private connection token.'
2725
+ );
2726
+ }
2727
+ const endpointCandidates = normalizeEndpointCandidates(result);
2728
+ if (endpointCandidates.length === 0) {
2729
+ throw createHubDiscoveryError(
2730
+ 'pin-registry-endpoints-missing',
2731
+ 'The PIN matched a Hub record without a reachable address.'
2732
+ );
2733
+ }
2734
+ const target = await chooseManagerConnectionTarget(endpointCandidates, {
2735
+ allowRelayFallback: options.allowRelayFallback === true,
2736
+ probeEndpoint: options.probeEndpoint
2737
+ });
2738
+ if (!target) {
2739
+ throw createHubDiscoveryError(
2740
+ 'pin-direct-unreachable',
2741
+ `No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
2742
+ );
2743
+ }
2744
+ return {
2745
+ manager: target.manager,
2746
+ pair: result.pair_token,
2747
+ hubDeviceId: String(result.node_id || '').trim(),
2748
+ endpointCandidates,
2749
+ directReachable: target.directReachable,
2750
+ connectionTransport: target.connectionTransport,
2751
+ discoverySource: 'supabase'
2752
+ };
2753
+ }
2652
2754
  });
2653
- if (!target) {
2654
- throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
2755
+ if (resolved.discoverySource === 'supabase') {
2756
+ writeCachedHubTarget(cacheOwnerKey, resolved);
2655
2757
  }
2656
-
2657
- const resolved = {
2658
- manager: target.manager,
2659
- pair: result.pair_token,
2660
- hubDeviceId: String(result.node_id || '').trim(),
2661
- endpointCandidates,
2662
- directReachable: target.directReachable,
2663
- connectionTransport: target.connectionTransport,
2664
- discoverySource: 'supabase'
2665
- };
2666
- writeCachedHubTarget(cacheOwnerKey, resolved);
2758
+ console.log(resolved.connectionTransport === 'relay-fallback' && resolved.discoverySource === 'cache'
2759
+ ? 'Fresh PIN registry lookup was unavailable; using the saved PIN-bound identity for the bounded encrypted relay path.'
2760
+ : resolved.discoverySource === 'cache'
2761
+ ? `Fresh PIN registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`
2762
+ : `Found current PIN-bound LiveDesk Hub at ${resolved.manager}.`);
2667
2763
  return resolved;
2668
- }
2764
+ }
2669
2765
 
2670
2766
  async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
2671
2767
  const configuredIntervalMs = Number(options.intervalMs || 0);
@@ -2680,7 +2776,8 @@ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
2680
2776
  attempts += 1;
2681
2777
  try {
2682
2778
  return await resolveManagerFromPin(supabase, pin, {
2683
- allowRelayFallback: options.allowRelayFallback === true
2779
+ allowRelayFallback: options.allowRelayFallback === true,
2780
+ relayEndpoint: options.relayEndpoint
2684
2781
  });
2685
2782
  } catch (err) {
2686
2783
  if (shouldStop()) {
@@ -2834,9 +2931,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2834
2931
  }
2835
2932
  dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
2836
2933
  try {
2837
- const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
2838
- shouldStop: () => completed || dashboardState.loggedOut
2839
- });
2934
+ const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
2935
+ allowRelayFallback: options.allowRelayFallback === true,
2936
+ relayEndpoint: options.relayEndpoint,
2937
+ shouldStop: () => completed || dashboardState.loggedOut
2938
+ });
2840
2939
  if (!resolved) {
2841
2940
  return;
2842
2941
  }
@@ -3066,7 +3165,8 @@ async function startConnectionChoiceServer(supabase, options = {}) {
3066
3165
  pin = normalizePairingPin(pin);
3067
3166
  try {
3068
3167
  const resolved = await resolveManagerFromPin(supabase, pin, {
3069
- allowRelayFallback: options.allowRelayFallback === true
3168
+ allowRelayFallback: options.allowRelayFallback === true,
3169
+ relayEndpoint: options.relayEndpoint
3070
3170
  });
3071
3171
  writeSavedPin(pin);
3072
3172
  applyStartupPreference(pendingStartup, startupArgs);
@@ -3229,7 +3329,8 @@ async function chooseClientConnection(supabase, options = {}) {
3229
3329
  resolvePin: pin => {
3230
3330
  if (!supabase) throw new Error('supabase-session-required');
3231
3331
  return resolveManagerFromPin(supabase, pin, {
3232
- allowRelayFallback: options.allowRelayFallback === true
3332
+ allowRelayFallback: options.allowRelayFallback === true,
3333
+ relayEndpoint: options.relayEndpoint
3233
3334
  });
3234
3335
  },
3235
3336
  changeRole: async (role, snapshot) => {
@@ -3328,7 +3429,8 @@ async function chooseClientConnection(supabase, options = {}) {
3328
3429
  startupArgs: options.startupArgs,
3329
3430
  savedSession: options.savedSession,
3330
3431
  savedPin: options.savedPin,
3331
- allowRelayFallback: options.allowRelayFallback === true
3432
+ allowRelayFallback: options.allowRelayFallback === true,
3433
+ relayEndpoint: options.relayEndpoint
3332
3434
  });
3333
3435
  if (options.openBrowser !== false) {
3334
3436
  console.log('Opening LiveDesk connection page...');
@@ -3506,10 +3608,16 @@ export async function chooseManagerConnectionTarget(candidates, options = {}) {
3506
3608
  }
3507
3609
 
3508
3610
  export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
3509
- const cached = options.cachedTarget || readCachedHubTarget(ownerKey);
3611
+ const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
3612
+ ? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
3613
+ : readCachedHubTarget(ownerKey);
3510
3614
  if (!cached) return null;
3511
- const manager = await chooseReachableEndpoint(cached.endpointCandidates, { requireReachable: true });
3512
- if (!manager) return null;
3615
+ const target = await chooseManagerConnectionTarget(cached.endpointCandidates, {
3616
+ allowRelayFallback: false,
3617
+ probeEndpoint: options.probeEndpoint
3618
+ });
3619
+ if (!target) return null;
3620
+ const manager = target.manager;
3513
3621
  const resolved = {
3514
3622
  manager,
3515
3623
  pair: cached.pair,
@@ -3518,6 +3626,8 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
3518
3626
  manager,
3519
3627
  ...cached.endpointCandidates.filter(endpoint => endpoint !== manager)
3520
3628
  ],
3629
+ directReachable: target.directReachable,
3630
+ connectionTransport: target.connectionTransport,
3521
3631
  discoverySource: 'cache'
3522
3632
  };
3523
3633
  if (options.persist !== false) {
@@ -3526,6 +3636,86 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
3526
3636
  return resolved;
3527
3637
  }
3528
3638
 
3639
+ export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
3640
+ if (options.allowRelayFallback !== true) return null;
3641
+ const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
3642
+ ? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
3643
+ : readCachedHubTarget(ownerKey);
3644
+ if (!cached) return null;
3645
+ const normalizedOwner = String(cached.ownerKey || '');
3646
+ const accountIdentityComplete = normalizedOwner.startsWith('user:')
3647
+ && normalizedOwner.length > 'user:'.length
3648
+ && Boolean(cached.hubDeviceId);
3649
+ // The public PIN resolver intentionally does not expose node_id. Its
3650
+ // previous signed pair/endpoints remain bound to the exact saved six-digit
3651
+ // PIN owner in the private, finite-lifetime cache.
3652
+ const pinIdentityComplete = /^pin:[0-9]{6}$/.test(normalizedOwner);
3653
+ const relayEndpoint = String(options.relayEndpoint || '').trim().replace(/^tcp:\/\//i, '');
3654
+ if ((!accountIdentityComplete && !pinIdentityComplete) || !parseManagerEndpoint(relayEndpoint)) {
3655
+ return null;
3656
+ }
3657
+ return {
3658
+ manager: cached.endpointCandidates[0],
3659
+ pair: cached.pair,
3660
+ hubDeviceId: cached.hubDeviceId,
3661
+ endpointCandidates: [...cached.endpointCandidates],
3662
+ directReachable: false,
3663
+ connectionTransport: 'relay-fallback',
3664
+ discoverySource: 'cache'
3665
+ };
3666
+ }
3667
+
3668
+ export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
3669
+ const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
3670
+ ? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
3671
+ : readCachedHubTarget(ownerKey);
3672
+ try {
3673
+ if (typeof options.resolveFreshTarget !== 'function') {
3674
+ throw createHubDiscoveryError('fresh-registry-resolver-required', 'Fresh Hub registry resolver is required.');
3675
+ }
3676
+ const configuredFreshTimeoutMs = Number(options.freshTimeoutMs);
3677
+ const freshTimeoutMs = Number.isFinite(configuredFreshTimeoutMs)
3678
+ ? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
3679
+ : FRESH_REGISTRY_TIMEOUT_MS;
3680
+ let timeoutHandle = null;
3681
+ const fresh = await Promise.race([
3682
+ Promise.resolve().then(() => options.resolveFreshTarget()),
3683
+ new Promise((_, reject) => {
3684
+ timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
3685
+ 'hub-registry-timeout',
3686
+ `Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
3687
+ )), freshTimeoutMs);
3688
+ })
3689
+ ]).finally(() => clearTimeout(timeoutHandle));
3690
+ if (!fresh?.manager || !fresh?.pair) {
3691
+ throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
3692
+ }
3693
+ return {
3694
+ ...fresh,
3695
+ discoverySource: fresh.discoverySource || 'supabase'
3696
+ };
3697
+ } catch (error) {
3698
+ if (canUseCachedRelayAfterRegistryError(error)) {
3699
+ const direct = await resolveManagerFromCachedTarget(ownerKey, {
3700
+ cachedTarget: cached,
3701
+ probeEndpoint: options.probeEndpoint,
3702
+ // A TCP listener is not pair-token authentication. Only a
3703
+ // fresh signed registry result may extend cache trust.
3704
+ persist: false
3705
+ });
3706
+ if (direct) return direct;
3707
+ const relay = resolveManagerFromCachedRelayFallback(ownerKey, {
3708
+ cachedTarget: cached,
3709
+ allowRelayFallback: options.allowRelayFallback === true,
3710
+ relayEndpoint: options.relayEndpoint,
3711
+ now: options.now
3712
+ });
3713
+ if (relay) return relay;
3714
+ }
3715
+ throw error;
3716
+ }
3717
+ }
3718
+
3529
3719
  export function getDiscoveryRetryDelay(attempt, configuredIntervalMs = 0, random = Math.random) {
3530
3720
  const configured = Number(configuredIntervalMs);
3531
3721
  if (Number.isFinite(configured) && configured > 0) {
@@ -3577,34 +3767,50 @@ function normalizeEndpointCandidates(target) {
3577
3767
  return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
3578
3768
  }
3579
3769
 
3580
- async function resolveManagerFromSupabase(supabase, options = {}) {
3770
+ export async function resolveManagerFromSupabase(supabase, options = {}) {
3581
3771
  await refreshSessionIfNeeded(supabase);
3582
3772
  const { data, error } = await supabase
3583
3773
  .from('livedesk_remote_host_targets')
3584
3774
  .select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
3585
3775
  .eq('product_key', 'livedesk')
3586
3776
  .maybeSingle();
3587
- if (error) {
3588
- throw error;
3589
- }
3590
- if (!data?.active) {
3591
- throw new Error('No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.');
3592
- }
3593
- if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
3594
- throw new Error('The LiveDesk Hub record is expired. Open the Hub screen again while signed in.');
3595
- }
3596
- if (!data.pair_token) {
3597
- throw new Error('The LiveDesk Hub record is missing its private connection token.');
3598
- }
3599
- const endpointCandidates = normalizeEndpointCandidates(data);
3600
- if (endpointCandidates.length === 0) {
3601
- throw new Error('The LiveDesk Hub record does not contain a reachable local address.');
3602
- }
3777
+ if (error) {
3778
+ throw error;
3779
+ }
3780
+ if (!data?.active) {
3781
+ throw createHubDiscoveryError(
3782
+ 'hub-registry-missing',
3783
+ 'No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.'
3784
+ );
3785
+ }
3786
+ if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
3787
+ throw createHubDiscoveryError(
3788
+ 'hub-registry-expired',
3789
+ 'The LiveDesk Hub record is expired. Open the Hub screen again while signed in.'
3790
+ );
3791
+ }
3792
+ if (!data.pair_token) {
3793
+ throw createHubDiscoveryError(
3794
+ 'hub-registry-pair-token-missing',
3795
+ 'The LiveDesk Hub record is missing its private connection token.'
3796
+ );
3797
+ }
3798
+ const endpointCandidates = normalizeEndpointCandidates(data);
3799
+ if (endpointCandidates.length === 0) {
3800
+ throw createHubDiscoveryError(
3801
+ 'hub-registry-endpoints-missing',
3802
+ 'The LiveDesk Hub record does not contain a reachable local address.'
3803
+ );
3804
+ }
3603
3805
  const target = await chooseManagerConnectionTarget(endpointCandidates, {
3604
- allowRelayFallback: options.allowRelayFallback === true
3806
+ allowRelayFallback: options.allowRelayFallback === true,
3807
+ probeEndpoint: options.probeEndpoint
3605
3808
  });
3606
3809
  if (!target) {
3607
- throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
3810
+ throw createHubDiscoveryError(
3811
+ 'hub-direct-unreachable',
3812
+ `No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
3813
+ );
3608
3814
  }
3609
3815
  return {
3610
3816
  manager: target.manager,
@@ -3647,6 +3853,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3647
3853
  let attempts = 0;
3648
3854
  let lastMessage = '';
3649
3855
  let wakeListener = null;
3856
+ let initialError = options.initialError || null;
3650
3857
  console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
3651
3858
  try {
3652
3859
  while (true) {
@@ -3655,6 +3862,11 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3655
3862
  }
3656
3863
  attempts += 1;
3657
3864
  try {
3865
+ if (initialError) {
3866
+ const error = initialError;
3867
+ initialError = null;
3868
+ throw error;
3869
+ }
3658
3870
  return await resolveManagerFromSupabase(supabase, {
3659
3871
  allowRelayFallback: options.allowRelayFallback === true
3660
3872
  });
@@ -3730,7 +3942,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3730
3942
  choice = { type: 'google', session: savedSession };
3731
3943
  } else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
3732
3944
  const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
3733
- allowRelayFallback
3945
+ allowRelayFallback,
3946
+ relayEndpoint: parsed.relay
3734
3947
  });
3735
3948
  choice = { type: 'pin', ...resolved };
3736
3949
  } else if (existingConnectionPage && previousChoice) {
@@ -3747,8 +3960,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3747
3960
  savedSession,
3748
3961
  savedPin,
3749
3962
  allowRelayFallback,
3963
+ relayEndpoint: parsed.relay,
3750
3964
  openBrowser: parsed.openBrowserOnStart
3751
- });
3965
+ });
3752
3966
  }
3753
3967
  connectionPage = existingConnectionPage || choice.connectionPage || null;
3754
3968
  if (choice.type === 'pin') {
@@ -3770,26 +3984,39 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3770
3984
  : 'Connected by LiveDesk PIN.');
3771
3985
  } else {
3772
3986
  const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
3773
- let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
3774
3987
  let session = choice.session;
3775
- discoverySource = resolved ? 'cache' : 'supabase';
3776
- if (resolved) {
3777
- connectionTransport = resolved.connectionTransport || 'direct-tcp';
3778
- directReachable = resolved.directReachable !== false;
3988
+ let initialDiscoveryError = null;
3989
+ let resolved = null;
3990
+ try {
3991
+ resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
3992
+ allowRelayFallback,
3993
+ relayEndpoint: parsed.relay,
3994
+ resolveFreshTarget: async () => {
3995
+ // The signed registry is authoritative for Hub replacement
3996
+ // and pair-token rotation. Cache direct/relay recovery is
3997
+ // considered only after this one current lookup reports
3998
+ // an explicitly recoverable availability failure.
3999
+ try {
4000
+ session = await activateSupabaseSession(supabase, choice.session);
4001
+ } catch (error) {
4002
+ if (!isRefreshTokenAlreadyUsedError(error)) throw error;
4003
+ session = readSavedSessionFromFile() || choice.session;
4004
+ console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
4005
+ }
4006
+ return await resolveManagerFromSupabase(supabase, {
4007
+ allowRelayFallback
4008
+ });
4009
+ }
4010
+ });
4011
+ } catch (error) {
4012
+ initialDiscoveryError = error;
4013
+ }
4014
+ discoverySource = resolved?.discoverySource || 'supabase';
4015
+ if (resolved?.discoverySource === 'cache') {
3779
4016
  writeSavedSessionToFile(session);
3780
- console.log(`Found cached LiveDesk Hub at ${resolved.manager}; skipped registry discovery.`);
3781
- } else {
3782
- // The browser and local runtime are separate auth owners.
3783
- // Activate the accepted browser session only when registry
3784
- // discovery is needed; a reachable account-bound cache can
3785
- // start the Agent without a Supabase round trip.
3786
- try {
3787
- session = await activateSupabaseSession(supabase, choice.session);
3788
- } catch (error) {
3789
- if (!isRefreshTokenAlreadyUsedError(error)) throw error;
3790
- session = readSavedSessionFromFile() || choice.session;
3791
- console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
3792
- }
4017
+ console.log(resolved.connectionTransport === 'relay-fallback'
4018
+ ? `Fresh Hub registry lookup was unavailable; using saved account-bound Hub identity ${resolved.hubDeviceId} for the bounded encrypted relay path.`
4019
+ : `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
3793
4020
  }
3794
4021
  choice.session = session;
3795
4022
  const email = session?.user?.email ? ` as ${session.user.email}` : '';
@@ -3797,18 +4024,20 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3797
4024
  if (!resolved) {
3798
4025
  resolved = await waitForManagerFromSupabase(supabase, {
3799
4026
  allowRelayFallback,
4027
+ initialError: initialDiscoveryError,
3800
4028
  shouldStop: () => Boolean(roleRestartRequest?.role)
3801
4029
  });
4030
+ discoverySource = 'supabase';
3802
4031
  }
3803
4032
  if (!resolved && roleRestartRequest?.role) {
3804
- return {
3805
- ...parsed,
3806
- manager,
3807
- pair,
3808
- slot: normalizeSlotNumber(parsed.slot),
3809
- connectionPage,
3810
- forwarded,
3811
- rediscoverOnDisconnect: shouldLogin,
4033
+ return {
4034
+ ...parsed,
4035
+ manager,
4036
+ pair,
4037
+ slot: normalizeSlotNumber(parsed.slot),
4038
+ connectionPage,
4039
+ forwarded,
4040
+ rediscoverOnDisconnect: shouldLogin,
3812
4041
  rediscoverOnInvalidPair: shouldLogin
3813
4042
  };
3814
4043
  }
@@ -4233,6 +4462,18 @@ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '
4233
4462
  const forwarded = [];
4234
4463
  for (let index = 0; index < args.length; index += 1) {
4235
4464
  const arg = args[index];
4465
+ if (RETIRED_CLIENT_MODEL_SINGLE_OPTIONS.has(arg)) {
4466
+ continue;
4467
+ }
4468
+ if (arg === RETIRED_CLIENT_MODEL_VALUE_OPTION) {
4469
+ if (index + 1 < args.length && !String(args[index + 1] || '').startsWith('-')) {
4470
+ index += 1;
4471
+ }
4472
+ continue;
4473
+ }
4474
+ if (String(arg || '').startsWith(`${RETIRED_CLIENT_MODEL_VALUE_OPTION}=`)) {
4475
+ continue;
4476
+ }
4236
4477
  if (arg === '--transport' || arg === '--relay') {
4237
4478
  index += 1;
4238
4479
  continue;
@@ -4241,8 +4482,8 @@ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '
4241
4482
  forwarded.push('--fake-frame');
4242
4483
  continue;
4243
4484
  }
4244
- if (arg === '--tasks' || arg === '--no-tasks' || arg === '--no-ai') {
4245
- continue;
4485
+ if (arg === '--tasks' || arg === '--no-tasks') {
4486
+ continue;
4246
4487
  }
4247
4488
  forwarded.push(arg);
4248
4489
  }
@@ -4446,15 +4687,15 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4446
4687
  });
4447
4688
  }
4448
4689
 
4449
- function shouldUseFast(parsed) {
4450
- if (parsed.engine === 'node') {
4451
- return false;
4690
+ function shouldUseFast(parsed) {
4691
+ if (parsed.engine === 'node') {
4692
+ return false;
4452
4693
  }
4453
4694
  if (parsed.engine === 'fast') {
4454
4695
  return true;
4455
4696
  }
4456
- return !parsed.nodeOnlyFeature;
4457
- }
4697
+ return true;
4698
+ }
4458
4699
 
4459
4700
  function shouldTryFast(parsed, runtime) {
4460
4701
  if (parsed.engine === 'fast') {
@@ -4480,6 +4721,12 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4480
4721
  console.warn('[LiveDesk] @livedesk/client is deprecated. Use the unified livedesk package or LiveDesk Desktop.');
4481
4722
  }
4482
4723
  const parsed = parseLauncherArgs(argv);
4724
+ if (parsed.retiredModelOptionsDiscarded > 0) {
4725
+ console.warn(
4726
+ '[LiveDesk Client] Ignored retired Client model-runtime options. '
4727
+ + 'Approved AI computer commands are handled by the Hub Codex Agent.'
4728
+ );
4729
+ }
4483
4730
  if (parsed.updateWaitPid) {
4484
4731
  await waitForProcessExit(parsed.updateWaitPid);
4485
4732
  }