livedesk 0.1.504 → 0.1.506

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/README.md CHANGED
@@ -4,8 +4,10 @@
4
4
  RemoteFast package for the current operating system and CPU, so Windows, macOS,
5
5
  and Linux native binaries are no longer downloaded together.
6
6
 
7
- One-command LiveDesk application. The same launcher resolves this computer's
8
- Hub or Client role, then starts only the runtime required by that role.
7
+ One-command LiveDesk application. The same launcher resolves this computer's
8
+ Hub or Client role. A Client starts one managed Agent. A Hub starts the Hub
9
+ server plus one headless Self Agent, so the Hub computer also appears on the
10
+ wall and can be controlled like the other computers.
9
11
 
10
12
  The `hub/`, `client/`, `runtime-core/`, and `web/` folders in this package are
11
13
  generated copies. Edit the source packages and run
@@ -43,7 +45,11 @@ npx -y --prefer-online livedesk@latest --force-role client
43
45
  npx -y --prefer-online livedesk@latest hub
44
46
  ```
45
47
 
46
- This starts the LiveDesk Hub, opens the local screen wall, and accepts clients.
48
+ This starts the LiveDesk Hub, opens the local screen wall, accepts clients, and
49
+ connects one local Self Agent through loopback TCP. The Self Agent reuses the
50
+ Hub computer's stable device identity and does not open another login or status
51
+ page. Hub shutdown, update, and role change drain that exact Agent tree before
52
+ the launcher exits or hands off.
47
53
  The Hub UI/API listens on `127.0.0.1` by default while the client endpoint
48
54
  continues to listen on the LAN.
49
55
 
package/bin/livedesk.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from 'node:module';
4
4
  import net from 'node:net';
@@ -46,7 +46,11 @@ import {
46
46
  readDeadWindowsOwnedProcessManifest,
47
47
  readWindowsOwnedProcessManifest
48
48
  } from '../client/src/runtime/windows-owned-process-manifest.js';
49
- import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
49
+ import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
50
+ import {
51
+ buildHubSelfAgentLaunchSpec,
52
+ createHubSelfAgentSupervisor
53
+ } from '../bootstrap/hub-self-agent.js';
50
54
 
51
55
  const require = createRequire(import.meta.url);
52
56
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -2536,9 +2540,47 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2536
2540
  let updateStopTimer = null;
2537
2541
  let updateHardStopTimer = null;
2538
2542
  let launcherShutdownSignal = '';
2539
- let launcherShutdownHardStopTimer = null;
2540
- let runtimeShutdownWatcher = null;
2541
- const launcherSignalHandlers = new Map();
2543
+ let launcherShutdownHardStopTimer = null;
2544
+ let runtimeShutdownWatcher = null;
2545
+ let selfAgentStopPromise = null;
2546
+ const launcherSignalHandlers = new Map();
2547
+
2548
+ let hubSelfAgent = null;
2549
+ try {
2550
+ const clientEntry = resolve(packageRoot, 'client', 'bin', 'livedesk-client.js');
2551
+ if (!existsSync(clientEntry)) {
2552
+ throw new Error('the packaged Client runtime is missing');
2553
+ }
2554
+ hubSelfAgent = createHubSelfAgentSupervisor({
2555
+ launch: buildHubSelfAgentLaunchSpec({
2556
+ nodePath: process.execPath,
2557
+ clientEntry,
2558
+ remotePort,
2559
+ pairToken,
2560
+ deviceId: env.LIVEDESK_DEVICE_ID,
2561
+ deviceName: env.LIVEDESK_DEVICE_NAME,
2562
+ stateDir: MANAGER_STATE_DIR,
2563
+ runtimeOwner,
2564
+ baseEnv: env
2565
+ }),
2566
+ report: reportLauncherUpdate
2567
+ });
2568
+ } catch (error) {
2569
+ reportLauncherUpdate(
2570
+ `[LiveDesk Hub] Self Agent is unavailable: ${error?.message || error}. Remote Clients remain available.`,
2571
+ true
2572
+ );
2573
+ }
2574
+
2575
+ const stopHubSelfAgent = reason => {
2576
+ if (!hubSelfAgent) return Promise.resolve({ ok: true, skipped: true });
2577
+ if (!selfAgentStopPromise) {
2578
+ selfAgentStopPromise = hubSelfAgent.stop(reason).finally(() => {
2579
+ selfAgentStopPromise = null;
2580
+ });
2581
+ }
2582
+ return selfAgentStopPromise;
2583
+ };
2542
2584
 
2543
2585
  const removeLauncherSignalHandlers = () => {
2544
2586
  for (const [signal, handler] of launcherSignalHandlers) {
@@ -2547,7 +2589,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2547
2589
  launcherSignalHandlers.clear();
2548
2590
  };
2549
2591
 
2550
- const requestLauncherShutdown = signal => {
2592
+ const requestLauncherShutdown = signal => {
2551
2593
  if (launcherShutdownSignal) return;
2552
2594
  launcherShutdownSignal = signal;
2553
2595
  restartRequest = null;
@@ -2559,15 +2601,18 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2559
2601
  clearTimeout(updateStopTimer);
2560
2602
  updateStopTimer = null;
2561
2603
  }
2562
- if (updateHardStopTimer) {
2604
+ if (updateHardStopTimer) {
2563
2605
  clearTimeout(updateHardStopTimer);
2564
- updateHardStopTimer = null;
2565
- }
2566
- const child = activeChild;
2567
- if (!child || child.exitCode !== null || child.signalCode) {
2568
- try { hubLogStream?.end(); } catch {}
2569
- process.exit(0);
2570
- return;
2606
+ updateHardStopTimer = null;
2607
+ }
2608
+ void stopHubSelfAgent(`launcher-${signal}`);
2609
+ const child = activeChild;
2610
+ if (!child || child.exitCode !== null || child.signalCode) {
2611
+ void stopHubSelfAgent(`launcher-${signal}`).finally(() => {
2612
+ try { hubLogStream?.end(); } catch {}
2613
+ process.exit(0);
2614
+ });
2615
+ return;
2571
2616
  }
2572
2617
  reportLauncherUpdate(
2573
2618
  `[LiveDesk Hub] Launcher received ${signal}; requesting graceful Hub shutdown for pid=${child.pid || 'unknown'}.`
@@ -2814,16 +2859,17 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2814
2859
  failPreflight(message);
2815
2860
  return;
2816
2861
  }
2817
- restartRequest = {
2862
+ restartRequest = {
2818
2863
  ...request,
2819
2864
  latestVersion: requestedVersion,
2820
2865
  pid: requestPid,
2821
2866
  operationId,
2822
2867
  _neutralCwd: neutralCwd,
2823
2868
  _originalCwd: UPDATE_ORIGINAL_CWD
2824
- };
2825
- reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${requestedVersion}.`);
2826
- const updateChild = activeChild;
2869
+ };
2870
+ reportLauncherUpdate(`[LiveDesk Hub] Restart requested for update ${request.operationId || 'unknown'} target=${requestedVersion}.`);
2871
+ void stopHubSelfAgent('hub-update');
2872
+ const updateChild = activeChild;
2827
2873
  let shutdownFallbackStarted = false;
2828
2874
  const forceStop = reason => {
2829
2875
  if (!updateChild || updateChild.exitCode !== null || updateChild.signalCode) return;
@@ -2864,7 +2910,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2864
2910
  }
2865
2911
  };
2866
2912
 
2867
- const startHubProcess = () => {
2913
+ const startHubProcess = () => {
2868
2914
  const stderrChunks = [];
2869
2915
  let stderrLength = 0;
2870
2916
  const child = spawn(process.execPath, [hubEntry, ...options.forwarded], {
@@ -2872,7 +2918,22 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2872
2918
  stdio: ['inherit', 'pipe', 'pipe'],
2873
2919
  windowsHide: !debugConsole
2874
2920
  });
2875
- activeChild = child;
2921
+ activeChild = child;
2922
+
2923
+ void waitForManager(hubProbeBaseUrl).then(ok => {
2924
+ if (!ok
2925
+ || activeChild !== child
2926
+ || child.exitCode !== null
2927
+ || child.signalCode
2928
+ || launcherShutdownSignal
2929
+ || restartRequest) return;
2930
+ hubSelfAgent?.start();
2931
+ }).catch(error => {
2932
+ reportLauncherUpdate(
2933
+ `[LiveDesk Hub] Self Agent startup wait failed: ${error?.message || error}.`,
2934
+ true
2935
+ );
2936
+ });
2876
2937
 
2877
2938
  child.stdout.on('data', chunk => {
2878
2939
  appendHubLog(chunk.toString());
@@ -2896,17 +2957,19 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2896
2957
  });
2897
2958
  child.once('exit', (code, signal) => {
2898
2959
  const startupOutput = stderrChunks.join('');
2899
- if (launcherShutdownSignal) {
2960
+ if (launcherShutdownSignal) {
2900
2961
  if (launcherShutdownHardStopTimer) {
2901
2962
  clearTimeout(launcherShutdownHardStopTimer);
2902
2963
  launcherShutdownHardStopTimer = null;
2903
2964
  }
2904
- runtimeShutdownWatcher?.stop();
2905
- removeLauncherSignalHandlers();
2906
- try { hubLogStream?.end(); } catch {}
2907
- process.exit(0);
2908
- return;
2909
- }
2965
+ runtimeShutdownWatcher?.stop();
2966
+ removeLauncherSignalHandlers();
2967
+ void stopHubSelfAgent(`launcher-${launcherShutdownSignal}`).finally(() => {
2968
+ try { hubLogStream?.end(); } catch {}
2969
+ process.exit(0);
2970
+ });
2971
+ return;
2972
+ }
2910
2973
  if (restartRequest) {
2911
2974
  const request = restartRequest;
2912
2975
  restartRequest = null;
@@ -2922,7 +2985,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2922
2985
  clearInterval(updatePollId);
2923
2986
  updatePollId = null;
2924
2987
  }
2925
- void restartWithLatest(request).catch(error => {
2988
+ void stopHubSelfAgent('hub-update').then(() => restartWithLatest(request)).catch(error => {
2926
2989
  const message = `The Hub restart handoff failed after shutdown: ${error instanceof Error ? error.message : String(error)}`;
2927
2990
  reportLauncherUpdate(`[LiveDesk Hub] ${message}. Restarting the unchanged Hub.`, true);
2928
2991
  try {
@@ -2949,7 +3012,7 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2949
3012
  } catch {
2950
3013
  // The unchanged Hub still restarts even if diagnostics are read-only.
2951
3014
  }
2952
- startHubProcess();
3015
+ startHubProcess();
2953
3016
  if (!updatePollId) {
2954
3017
  updatePollId = setInterval(pollUpdateRequest, HUB_UPDATE_POLL_MS);
2955
3018
  updatePollId.unref?.();
@@ -2957,39 +3020,44 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
2957
3020
  });
2958
3021
  return;
2959
3022
  }
2960
- if (!signal && code === ROLE_TRANSITION_EXIT_CODE) {
2961
- if (updatePollId) {
2962
- clearInterval(updatePollId);
2963
- updatePollId = null;
2964
- }
2965
- try {
2966
- runtimeLock?.updateRole?.('client');
2967
- } catch (error) {
2968
- console.error(
2969
- `[LiveDesk Hub] The shared runtime lock could not transition to Client: ${error?.message || error}. `
2970
- + 'The launcher will exit so a clean invocation can recover.'
2971
- );
2972
- runtimeShutdownWatcher?.stop();
2973
- removeLauncherSignalHandlers();
2974
- try { hubLogStream?.end(); } catch {}
2975
- process.exit(1);
2976
- return;
2977
- }
2978
- writeRoleCache({
2979
- role: 'client',
2980
- deviceId: resolvedRole?.deviceId || process.env.LIVEDESK_DEVICE_ID || '',
2981
- deviceName: resolvedRole?.deviceName || os.hostname(),
2982
- source: 'supabase',
2983
- isOfflineFallback: false
2984
- }, { stateDir: MANAGER_STATE_DIR });
2985
- console.log('[LiveDesk Hub] Role transition accepted. Starting the Client runtime.');
2986
- runtimeShutdownWatcher?.stop();
2987
- removeLauncherSignalHandlers();
2988
- void runClient([], { ...resolvedRole, role: 'client', source: 'supabase', roleTransition: true }, runtimeLock).catch(error => {
2989
- console.error(`Failed to start LiveDesk Client runtime: ${error?.message || error}`);
2990
- process.exitCode = 1;
2991
- });
2992
- return;
3023
+ if (!signal && code === ROLE_TRANSITION_EXIT_CODE) {
3024
+ if (updatePollId) {
3025
+ clearInterval(updatePollId);
3026
+ updatePollId = null;
3027
+ }
3028
+ void stopHubSelfAgent('role-transition').then(() => {
3029
+ try {
3030
+ runtimeLock?.updateRole?.('client');
3031
+ } catch (error) {
3032
+ console.error(
3033
+ `[LiveDesk Hub] The shared runtime lock could not transition to Client: ${error?.message || error}. `
3034
+ + 'The launcher will exit so a clean invocation can recover.'
3035
+ );
3036
+ runtimeShutdownWatcher?.stop();
3037
+ removeLauncherSignalHandlers();
3038
+ try { hubLogStream?.end(); } catch {}
3039
+ process.exit(1);
3040
+ return;
3041
+ }
3042
+ writeRoleCache({
3043
+ role: 'client',
3044
+ deviceId: resolvedRole?.deviceId || process.env.LIVEDESK_DEVICE_ID || '',
3045
+ deviceName: resolvedRole?.deviceName || os.hostname(),
3046
+ source: 'supabase',
3047
+ isOfflineFallback: false
3048
+ }, { stateDir: MANAGER_STATE_DIR });
3049
+ console.log('[LiveDesk Hub] Role transition accepted. Starting the Client runtime.');
3050
+ runtimeShutdownWatcher?.stop();
3051
+ removeLauncherSignalHandlers();
3052
+ void runClient([], { ...resolvedRole, role: 'client', source: 'supabase', roleTransition: true }, runtimeLock).catch(error => {
3053
+ console.error(`Failed to start LiveDesk Client runtime: ${error?.message || error}`);
3054
+ process.exitCode = 1;
3055
+ });
3056
+ }).catch(error => {
3057
+ console.error(`[LiveDesk Hub] Self Agent did not stop for role transition: ${error?.message || error}`);
3058
+ process.exit(1);
3059
+ });
3060
+ return;
2993
3061
  }
2994
3062
  if (!signal
2995
3063
  && code !== 0
@@ -3011,11 +3079,11 @@ async function runManager(args, resolvedRole = null, runtimeLock = null, updateH
3011
3079
  });
3012
3080
  return;
3013
3081
  }
3014
- if (signal) {
3015
- process.kill(process.pid, signal);
3016
- return;
3017
- }
3018
- process.exit(code ?? 0);
3082
+ if (signal) {
3083
+ void stopHubSelfAgent(`hub-signal-${signal}`).finally(() => process.kill(process.pid, signal));
3084
+ return;
3085
+ }
3086
+ void stopHubSelfAgent('hub-exited').finally(() => process.exit(code ?? 0));
3019
3087
  });
3020
3088
  };
3021
3089
 
@@ -3321,12 +3389,12 @@ async function main() {
3321
3389
  await stopProcessesOnPorts([replacementCleanup.runtimePort, remotePort]);
3322
3390
  }
3323
3391
 
3324
- if (resolvedRole.role === 'client') {
3325
- // A launcher and Agent can both die while an independently spawned FFmpeg
3326
- // survives. Before any new Client process starts, consume only the exact
3327
- // manifest generation returned by the stale-lock compare-and-swap. No
3328
- // process-name scan participates in this recovery path.
3329
- const reclaimedManifestDrain = await drainReclaimedWindowsClientManifest(lock.reclaimedOwner);
3392
+ if (resolvedRole.role === 'client' || resolvedRole.role === 'hub') {
3393
+ // A launcher and Agent can both die while an independently spawned capture
3394
+ // helper survives. This applies to a normal Client and to the Hub-owned
3395
+ // Self Agent. Consume only the exact manifest generation returned by the
3396
+ // stale-lock compare-and-swap; no process-name scan participates here.
3397
+ const reclaimedManifestDrain = await drainReclaimedWindowsClientManifest(lock.reclaimedOwner);
3330
3398
  const testHoldAfterDrainMs = Number(
3331
3399
  process.env.LIVEDESK_TEST_HOLD_AFTER_RECLAIMED_WINDOWS_DRAIN_MS || 0
3332
3400
  );
@@ -3334,9 +3402,12 @@ async function main() {
3334
3402
  && process.env.LIVEDESK_DEBUG === '1'
3335
3403
  && Number.isFinite(testHoldAfterDrainMs)
3336
3404
  && testHoldAfterDrainMs > 0) {
3337
- console.log('[LiveDesk test] Holding after reclaimed manifest drain before Client startup.');
3338
- await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
3339
- }
3405
+ console.log('[LiveDesk test] Holding after reclaimed manifest drain before Agent startup.');
3406
+ await waitMilliseconds(Math.min(60_000, Math.round(testHoldAfterDrainMs)));
3407
+ }
3408
+ }
3409
+
3410
+ if (resolvedRole.role === 'client') {
3340
3411
  const clientRuntimePort = normalizePort(
3341
3412
  process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_LOCAL_RUNTIME_PORT,
3342
3413
  DEFAULT_MANAGER_HTTP_PORT
@@ -0,0 +1,243 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+
4
+ const SELF_AGENT_SHUTDOWN_MESSAGE = 'livedesk.self-agent.shutdown';
5
+ const SELF_AGENT_READY_MESSAGE = 'livedesk.self-agent.ready';
6
+
7
+ function normalizeText(value) {
8
+ return String(value || '').trim();
9
+ }
10
+
11
+ function normalizePort(value) {
12
+ const port = Number(value);
13
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 0;
14
+ }
15
+
16
+ export function buildHubSelfAgentLaunchSpec({
17
+ nodePath = process.execPath,
18
+ clientEntry,
19
+ remotePort,
20
+ pairToken,
21
+ deviceId,
22
+ deviceName,
23
+ stateDir,
24
+ runtimeOwner = null,
25
+ baseEnv = process.env
26
+ } = {}) {
27
+ const normalizedNodePath = normalizeText(nodePath);
28
+ const normalizedClientEntry = normalizeText(clientEntry);
29
+ const normalizedRemotePort = normalizePort(remotePort);
30
+ const normalizedPairToken = normalizeText(pairToken);
31
+ const normalizedDeviceId = normalizeText(deviceId);
32
+ const normalizedDeviceName = normalizeText(deviceName);
33
+ const normalizedStateDir = normalizeText(stateDir);
34
+ if (!normalizedNodePath) throw new Error('Hub Self Agent requires the current Node executable.');
35
+ if (!normalizedClientEntry) throw new Error('Hub Self Agent requires the packaged Client entry.');
36
+ if (!normalizedRemotePort) throw new Error('Hub Self Agent requires a valid local Hub Client port.');
37
+ if (!normalizedPairToken) throw new Error('Hub Self Agent requires the current Hub pair token.');
38
+ if (!normalizedDeviceId) throw new Error('Hub Self Agent requires the Hub device identity.');
39
+
40
+ const manager = `127.0.0.1:${normalizedRemotePort}`;
41
+ const env = {
42
+ ...baseEnv,
43
+ LIVEDESK_HUB_SELF_AGENT: '1',
44
+ LIVEDESK_NPM_LAUNCHER_NAME: 'livedesk',
45
+ LIVEDESK_NPM_LAUNCHER_VERSION: String(
46
+ baseEnv.LIVEDESK_MANAGER_VERSION || baseEnv.LIVEDESK_NPM_LAUNCHER_VERSION || ''
47
+ ),
48
+ LIVEDESK_UNIFIED_RUNTIME: '1',
49
+ LIVEDESK_RUNTIME_ROLE: 'client',
50
+ LIVEDESK_ROLE_SOURCE: 'hub-self-agent',
51
+ LIVEDESK_SKIP_BROWSER_OPEN: '1',
52
+ LIVEDESK_CLIENT_ENGINE: 'fast',
53
+ LIVEDESK_CLIENT_REQUIRE_FAST: '1',
54
+ LIVEDESK_CLIENT_TRANSPORT: 'tcp',
55
+ LIVEDESK_CLIENT_MANAGER: manager,
56
+ LIVEDESK_CLIENT_PAIR_TOKEN: normalizedPairToken,
57
+ LIVEDESK_DEVICE_ID: normalizedDeviceId,
58
+ LIVEDESK_DEVICE_NAME: normalizedDeviceName,
59
+ LIVEDESK_CLIENT_NAME: normalizedDeviceName,
60
+ LIVEDESK_STATE_DIR: normalizedStateDir,
61
+ LIVEDESK_CLIENT_UPDATE_STATE_PATH: normalizedStateDir
62
+ ? join(normalizedStateDir, 'hub-self-agent-update.json')
63
+ : '',
64
+ LIVEDESK_RUNTIME_OWNER_PID: String(runtimeOwner?.pid || ''),
65
+ LIVEDESK_RUNTIME_OWNER_TOKEN: String(runtimeOwner?.ownerToken || ''),
66
+ LIVEDESK_RUNTIME_OWNER_INSTANCE_MARKER: String(runtimeOwner?.ownerInstanceMarker || ''),
67
+ LIVEDESK_RUNTIME_OWNER_START_ORDER: String(runtimeOwner?.ownerStartOrder || '')
68
+ };
69
+
70
+ return {
71
+ command: normalizedNodePath,
72
+ args: [
73
+ normalizedClientEntry,
74
+ '--no-login',
75
+ '--no-open',
76
+ '--fast',
77
+ '--transport',
78
+ 'tcp',
79
+ '--device-id',
80
+ normalizedDeviceId
81
+ ],
82
+ options: {
83
+ env,
84
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
85
+ windowsHide: true
86
+ },
87
+ manager
88
+ };
89
+ }
90
+
91
+ export function createHubSelfAgentSupervisor({
92
+ launch,
93
+ spawnImpl = spawn,
94
+ report = () => undefined,
95
+ restartBaseDelayMs = 1_000,
96
+ restartMaxDelayMs = 8_000,
97
+ shutdownTimeoutMs = 12_000
98
+ } = {}) {
99
+ if (!launch || typeof launch !== 'object') {
100
+ throw new TypeError('Hub Self Agent launch specification is required.');
101
+ }
102
+
103
+ let child = null;
104
+ let restartTimer = null;
105
+ let stopping = false;
106
+ let consecutiveFailures = 0;
107
+ let stopPromise = null;
108
+
109
+ const clearRestartTimer = () => {
110
+ if (!restartTimer) return;
111
+ clearTimeout(restartTimer);
112
+ restartTimer = null;
113
+ };
114
+
115
+ const scheduleRestart = () => {
116
+ if (stopping || restartTimer) return;
117
+ consecutiveFailures += 1;
118
+ const delayMs = Math.min(
119
+ Math.max(100, Number(restartMaxDelayMs) || 8_000),
120
+ Math.max(100, Number(restartBaseDelayMs) || 1_000) * (2 ** Math.min(3, consecutiveFailures - 1))
121
+ );
122
+ report(`[LiveDesk Hub] Self Agent stopped unexpectedly; retrying in ${delayMs}ms.`, true);
123
+ restartTimer = setTimeout(() => {
124
+ restartTimer = null;
125
+ start();
126
+ }, delayMs);
127
+ restartTimer.unref?.();
128
+ };
129
+
130
+ const start = () => {
131
+ stopping = false;
132
+ clearRestartTimer();
133
+ if (child && child.exitCode === null && !child.signalCode) return false;
134
+
135
+ let spawned;
136
+ try {
137
+ spawned = spawnImpl(launch.command, launch.args, launch.options);
138
+ } catch (error) {
139
+ report(`[LiveDesk Hub] Self Agent launch failed: ${error?.message || error}.`, true);
140
+ scheduleRestart();
141
+ return false;
142
+ }
143
+ child = spawned;
144
+ let settled = false;
145
+
146
+ spawned.stdout?.on?.('data', chunk => process.stdout.write(chunk));
147
+ spawned.stderr?.on?.('data', chunk => process.stderr.write(chunk));
148
+ spawned.on?.('message', message => {
149
+ if (message?.type !== SELF_AGENT_READY_MESSAGE) return;
150
+ consecutiveFailures = 0;
151
+ report(`[LiveDesk Hub] Self Agent ready pid=${spawned.pid || 'unknown'} manager=${launch.manager}.`);
152
+ });
153
+ spawned.once?.('error', error => {
154
+ if (settled) return;
155
+ settled = true;
156
+ if (child === spawned) child = null;
157
+ report(`[LiveDesk Hub] Self Agent process error: ${error?.message || error}.`, true);
158
+ scheduleRestart();
159
+ });
160
+ spawned.once?.('exit', (code, signal) => {
161
+ if (settled) return;
162
+ settled = true;
163
+ if (child === spawned) child = null;
164
+ if (!stopping) {
165
+ report(
166
+ `[LiveDesk Hub] Self Agent exited code=${code ?? 'none'} signal=${signal || 'none'}.`,
167
+ true
168
+ );
169
+ scheduleRestart();
170
+ }
171
+ });
172
+ report(`[LiveDesk Hub] Starting Self Agent pid=${spawned.pid || 'pending'} manager=${launch.manager}.`);
173
+ return true;
174
+ };
175
+
176
+ const stop = (reason = 'hub-stopping') => {
177
+ stopping = true;
178
+ clearRestartTimer();
179
+ if (stopPromise) return stopPromise;
180
+ const ownedChild = child;
181
+ if (!ownedChild || ownedChild.exitCode !== null || ownedChild.signalCode) {
182
+ child = null;
183
+ return Promise.resolve({ ok: true, alreadyStopped: true });
184
+ }
185
+
186
+ stopPromise = new Promise(resolveStop => {
187
+ let finished = false;
188
+ let hardStopTimer = null;
189
+ const finish = result => {
190
+ if (finished) return;
191
+ finished = true;
192
+ if (hardStopTimer) clearTimeout(hardStopTimer);
193
+ if (child === ownedChild) child = null;
194
+ stopPromise = null;
195
+ resolveStop(result);
196
+ };
197
+ ownedChild.once?.('exit', (code, signal) => finish({ ok: true, code, signal }));
198
+ try {
199
+ ownedChild.send?.({
200
+ type: SELF_AGENT_SHUTDOWN_MESSAGE,
201
+ signal: 'SIGTERM',
202
+ reason: normalizeText(reason).slice(0, 120)
203
+ });
204
+ } catch (error) {
205
+ report(`[LiveDesk Hub] Self Agent shutdown message failed: ${error?.message || error}.`, true);
206
+ }
207
+ hardStopTimer = setTimeout(() => {
208
+ if (ownedChild.exitCode !== null || ownedChild.signalCode) {
209
+ finish({ ok: true });
210
+ return;
211
+ }
212
+ report(
213
+ `[LiveDesk Hub] Self Agent pid=${ownedChild.pid || 'unknown'} exceeded graceful shutdown; force-stopping that exact child root.`,
214
+ true
215
+ );
216
+ try {
217
+ ownedChild.kill('SIGKILL');
218
+ } catch (error) {
219
+ finish({ ok: false, error });
220
+ }
221
+ }, Math.max(1_000, Number(shutdownTimeoutMs) || 12_000));
222
+ hardStopTimer.unref?.();
223
+ });
224
+ return stopPromise;
225
+ };
226
+
227
+ return {
228
+ start,
229
+ stop,
230
+ getStatus: () => ({
231
+ running: !!child && child.exitCode === null && !child.signalCode,
232
+ pid: Number(child?.pid || 0),
233
+ stopping,
234
+ restartScheduled: !!restartTimer,
235
+ consecutiveFailures
236
+ })
237
+ };
238
+ }
239
+
240
+ export const HUB_SELF_AGENT_MESSAGES = Object.freeze({
241
+ shutdown: SELF_AGENT_SHUTDOWN_MESSAGE,
242
+ ready: SELF_AGENT_READY_MESSAGE
243
+ });
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import net from 'net';
4
4
  import os from 'os';