@livedesk/client 0.1.170 → 0.1.172

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.
@@ -43,13 +43,93 @@ function normalizeClientUpdateTargetVersion(value) {
43
43
  }
44
44
 
45
45
  function getClientUpdateStatePath() {
46
- return String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'));
46
+ return path.resolve(
47
+ String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(os.homedir(), '.livedesk', 'client-update.json'))
48
+ );
49
+ }
50
+
51
+ function getClientUpdateNeutralCwd(operationId) {
52
+ const safeOperationId = String(operationId || '')
53
+ .replace(/[^A-Za-z0-9._-]/g, '_')
54
+ .slice(0, 120) || 'unknown';
55
+ return path.join(os.tmpdir(), 'livedesk-update-cwd', `client-${safeOperationId}`);
56
+ }
57
+
58
+ async function prepareClientUpdateNeutralCwd(operationId) {
59
+ const neutralCwd = path.resolve(getClientUpdateNeutralCwd(operationId));
60
+ await fs.mkdir(neutralCwd, { recursive: true });
61
+ for (let ancestor = neutralCwd; ; ancestor = path.dirname(ancestor)) {
62
+ const shadowPath = [
63
+ path.join(ancestor, 'package.json'),
64
+ path.join(ancestor, 'node_modules', 'livedesk', 'package.json')
65
+ ].find(candidate => existsSync(candidate));
66
+ if (shadowPath) {
67
+ throw new Error(`LiveDesk update neutral working directory is shadowed by ${shadowPath}: ${neutralCwd}`);
68
+ }
69
+ const parent = path.dirname(ancestor);
70
+ if (parent === ancestor) break;
71
+ }
72
+ return neutralCwd;
73
+ }
74
+
75
+ function buildClientUpdateNpxEnvironment(baseEnv, neutralCwd) {
76
+ const env = { ...baseEnv };
77
+ const isolatedNames = new Set([
78
+ 'init_cwd',
79
+ 'npm_config_local_prefix',
80
+ 'npm_config_workspace',
81
+ 'npm_config_workspaces',
82
+ 'npm_config_include_workspace_root',
83
+ 'npm_package_json',
84
+ 'npm_lifecycle_event',
85
+ 'npm_lifecycle_script'
86
+ ]);
87
+ for (const key of Object.keys(env)) {
88
+ if (isolatedNames.has(key.toLowerCase())) delete env[key];
89
+ }
90
+ env.INIT_CWD = neutralCwd;
91
+ env.npm_config_local_prefix = neutralCwd;
92
+ env.npm_config_include_workspace_root = 'false';
93
+ return env;
94
+ }
95
+
96
+ function isClientUpdatePidAlive(value) {
97
+ const pid = Number(value);
98
+ if (!Number.isInteger(pid) || pid <= 1) return false;
99
+ try {
100
+ process.kill(pid, 0);
101
+ return true;
102
+ } catch (error) {
103
+ return error?.code === 'EPERM';
104
+ }
105
+ }
106
+
107
+ function canReclaimClientUpdateOperation(previous) {
108
+ const handoffOwners = [
109
+ previous?.supervisorPid,
110
+ previous?.starterPid
111
+ ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
112
+ if (handoffOwners.length > 0) {
113
+ return handoffOwners.every(pid => !isClientUpdatePidAlive(pid));
114
+ }
115
+ if (String(previous?.stage || '') === 'scheduled') {
116
+ const schedulingOwners = [
117
+ previous?.agentPid,
118
+ previous?.launcherPid
119
+ ].map(Number).filter(pid => Number.isInteger(pid) && pid > 1);
120
+ return schedulingOwners.length > 0
121
+ && schedulingOwners.every(pid => !isClientUpdatePidAlive(pid));
122
+ }
123
+ return false;
47
124
  }
48
125
 
49
126
  async function writeClientUpdateState(stage, details = {}) {
50
127
  const statePath = getClientUpdateStatePath();
51
128
  const operationId = String(details.operationId || process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
52
129
  if (!operationId) return;
130
+ const attemptId = stage === 'scheduled'
131
+ ? ''
132
+ : String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
53
133
  let previous = null;
54
134
  try {
55
135
  previous = JSON.parse(await fs.readFile(statePath, 'utf8'));
@@ -60,11 +140,28 @@ async function writeClientUpdateState(stage, details = {}) {
60
140
  && previous.operationId !== operationId
61
141
  && stage === 'scheduled'
62
142
  && ['connected', 'failed', 'restored'].includes(String(previous.stage || ''));
63
- if (previous?.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return;
143
+ const reclaimsDeadOperation = previous?.operationId
144
+ && previous.operationId !== operationId
145
+ && stage === 'scheduled'
146
+ && canReclaimClientUpdateOperation(previous);
147
+ const replacesPreviousOperation = replacesTerminalOperation || reclaimsDeadOperation;
148
+ if (previous?.operationId && previous.operationId !== operationId && !replacesPreviousOperation) return;
149
+ if (!replacesPreviousOperation
150
+ && previous?.attemptId
151
+ && attemptId
152
+ && previous.attemptId !== attemptId) {
153
+ return;
154
+ }
155
+ if (!replacesPreviousOperation
156
+ && previous?.stage === 'connected'
157
+ && previous?.restartVerified === true
158
+ && stage !== 'connected') {
159
+ return;
160
+ }
64
161
  const targetProductVersion = String(
65
162
  details.targetProductVersion
66
163
  || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION
67
- || (!replacesTerminalOperation ? previous?.targetProductVersion : '')
164
+ || (!replacesPreviousOperation ? previous?.targetProductVersion : '')
68
165
  || ''
69
166
  ).trim();
70
167
  const targetAgentVersion = String(
@@ -72,12 +169,12 @@ async function writeClientUpdateState(stage, details = {}) {
72
169
  || details.targetVersion
73
170
  || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION
74
171
  || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION
75
- || (!replacesTerminalOperation ? previous?.targetAgentVersion : '')
76
- || (!replacesTerminalOperation ? previous?.targetVersion : '')
172
+ || (!replacesPreviousOperation ? previous?.targetAgentVersion : '')
173
+ || (!replacesPreviousOperation ? previous?.targetVersion : '')
77
174
  || ''
78
175
  ).trim();
79
176
  const state = {
80
- ...(!replacesTerminalOperation && previous && typeof previous === 'object' ? previous : {}),
177
+ ...(!replacesPreviousOperation && previous && typeof previous === 'object' ? previous : {}),
81
178
  operationId,
82
179
  stage,
83
180
  targetVersion: targetAgentVersion,
@@ -87,7 +184,7 @@ async function writeClientUpdateState(stage, details = {}) {
87
184
  agentVersion: AGENT_VERSION,
88
185
  agentPid: process.pid,
89
186
  launcherPid: Number(process.env.LIVEDESK_CLIENT_PARENT_PID || 0) || 0,
90
- attemptId: String(details.attemptId || process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim(),
187
+ attemptId,
91
188
  updatedAt: new Date().toISOString(),
92
189
  ...details
93
190
  };
@@ -95,15 +192,33 @@ async function writeClientUpdateState(stage, details = {}) {
95
192
  const temporaryPath = `${statePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
96
193
  try {
97
194
  await fs.writeFile(temporaryPath, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
195
+ let current = null;
196
+ try {
197
+ current = JSON.parse(await fs.readFile(statePath, 'utf8'));
198
+ } catch {
199
+ // A missing first-write state is still owned by this transition.
200
+ }
201
+ if (String(current?.operationId || '') !== String(previous?.operationId || '')
202
+ || String(current?.stage || '') !== String(previous?.stage || '')
203
+ || String(current?.updatedAt || '') !== String(previous?.updatedAt || '')) {
204
+ return;
205
+ }
98
206
  await fs.rename(temporaryPath, statePath);
99
207
  } finally {
100
208
  await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
101
209
  }
102
210
  }
103
211
 
104
- async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_000) {
212
+ async function waitForClientUpdateShutdownSignal(
213
+ operationId,
214
+ updateDeadlineEpochMs,
215
+ timeoutMs = 180_000
216
+ ) {
105
217
  const statePath = getClientUpdateStatePath();
106
- const deadline = Date.now() + timeoutMs;
218
+ const deadline = Math.min(
219
+ Date.now() + timeoutMs,
220
+ Number(updateDeadlineEpochMs) || 0
221
+ );
107
222
  while (Date.now() < deadline) {
108
223
  try {
109
224
  const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
@@ -111,22 +226,32 @@ async function waitForClientUpdateShutdownSignal(operationId, timeoutMs = 150_00
111
226
  await wait(200);
112
227
  continue;
113
228
  }
114
- if (state.stage === 'waiting-for-shutdown') return true;
229
+ if (state.stage === 'preflight-ready' || state.stage === 'waiting-for-shutdown') {
230
+ return { ready: true, error: '' };
231
+ }
115
232
  if (state.stage === 'failed') {
116
- console.error(`LiveDesk client update preparation failed: ${state.error || 'unknown error'}`);
117
- return false;
233
+ const error = String(state.error || 'LiveDesk client update preparation failed.');
234
+ console.error(`LiveDesk client update preparation failed: ${error}`);
235
+ return { ready: false, error };
118
236
  }
119
237
  } catch {
120
238
  // The detached supervisor may still be writing its first stage.
121
239
  }
122
240
  await wait(200);
123
241
  }
242
+ const deadlineExpired = Date.now() >= Number(updateDeadlineEpochMs);
243
+ const error = deadlineExpired
244
+ ? 'The absolute LiveDesk Client update deadline expired before shutdown.'
245
+ : 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.';
124
246
  await writeClientUpdateState('failed', {
125
247
  operationId,
126
- error: 'Timed out waiting for the LiveDesk update supervisor to prepare the exact package.'
248
+ error,
249
+ cancelRequested: true,
250
+ deadlineExpired,
251
+ failedAt: new Date().toISOString()
127
252
  }).catch(() => undefined);
128
253
  console.error('LiveDesk client update preparation timed out; the current Agent will remain running.');
129
- return false;
254
+ return { ready: false, error };
130
255
  }
131
256
 
132
257
  function markClientUpdateConnected() {
@@ -1432,55 +1557,43 @@ function remotePolicyAllows(options, command) {
1432
1557
 
1433
1558
  function buildClientUpdateBootstrapScript() {
1434
1559
  return [
1435
- 'const { spawn, spawnSync } = require("node:child_process");',
1560
+ 'const { spawn } = require("node:child_process");',
1436
1561
  'const fs = require("node:fs");',
1437
1562
  'const path = require("node:path");',
1438
- 'const crypto = require("node:crypto");',
1439
- 'const waitPid = Number(process.env.LIVEDESK_UPDATE_WAIT_PID || 0);',
1440
- 'const agentPid = Number(process.env.LIVEDESK_UPDATE_AGENT_PID || 0);',
1441
1563
  'const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || "").trim();',
1442
- 'const statePath = String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json"));',
1443
- 'const logPath = statePath.replace(/\\.json$/i, "") + ".log";',
1564
+ 'const originalCwd = path.resolve(String(process.env.LIVEDESK_UPDATE_ORIGINAL_CWD || process.cwd()));',
1565
+ 'const statePath = path.resolve(originalCwd, String(process.env.LIVEDESK_CLIENT_UPDATE_STATE_PATH || path.join(require("node:os").homedir(), ".livedesk", "client-update.json")));',
1566
+ 'const neutralCwdValue = String(process.env.LIVEDESK_UPDATE_NEUTRAL_CWD || "").trim();',
1567
+ 'const neutralCwd = neutralCwdValue ? path.resolve(neutralCwdValue) : "";',
1444
1568
  'const cleanVersion = value => String(value || "").trim().replace(/^v/i, "");',
1445
- 'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
1446
1569
  'const targetProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION);',
1447
- 'const targetAgentVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION || process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || targetProductVersion);',
1448
- 'const currentProductVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION || process.env.LIVEDESK_NPM_LAUNCHER_VERSION);',
1449
- 'const currentAgentVersion = cleanVersion(process.env.LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION || process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || currentProductVersion);',
1450
- 'for (const [label, version] of [["target product", targetProductVersion], ["target Agent", targetAgentVersion], ["current product", currentProductVersion], ["current Agent", currentAgentVersion]]) { if (!versionPattern.test(version)) throw new Error("Invalid LiveDesk " + label + " update version: " + version); }',
1451
- 'const packageSpec = "livedesk@" + targetProductVersion;',
1452
- 'const localLauncher = String(process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER || process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY || "").trim();',
1453
- 'const preservedArgs = (() => { try { const decoded = JSON.parse(Buffer.from(String(process.env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 || ""), "base64").toString("utf8")); return Array.isArray(decoded) ? decoded.map(value => String(value)) : []; } catch { return []; } })();',
1454
- 'const proofTimeoutMs = Math.max(500, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_TIMEOUT_MS || 30000));',
1455
- 'const proofPollMs = Math.max(25, Number(process.env.LIVEDESK_CLIENT_UPDATE_PROOF_POLL_MS || 200));',
1456
- 'const killGraceMs = Math.max(50, Number(process.env.LIVEDESK_CLIENT_UPDATE_KILL_GRACE_MS || 1500));',
1457
- 'const targetLaunchLimit = 3;',
1458
- 'const restoreLaunchLimit = 2;',
1570
+ 'const updateDeadlineEpochMs = Number(process.env.LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS || 0);',
1571
+ 'const deadlineExpired = () => !Number.isSafeInteger(updateDeadlineEpochMs) || Date.now() >= updateDeadlineEpochMs;',
1572
+ 'const versionPattern = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;',
1459
1573
  'const readState = () => { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return {}; } };',
1460
- 'const writeState = (stage, details = {}) => { fs.mkdirSync(path.dirname(statePath), { recursive: true }); const previous = readState(); const replacesTerminalOperation = previous.operationId && previous.operationId !== operationId && stage === "preparing-package" && ["connected", "failed", "restored"].includes(String(previous.stage || "")); if (previous.operationId && previous.operationId !== operationId && !replacesTerminalOperation) return false; const base = replacesTerminalOperation ? {} : previous; const next = { ...base, operationId, stage, targetVersion: targetAgentVersion, targetProductVersion, targetAgentVersion, currentProductVersion, currentAgentVersion, supervisorPid: process.pid, updatedAt: new Date().toISOString(), ...details }; const temporary = statePath + "." + process.pid + "." + Math.random().toString(16).slice(2) + ".tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return true; };',
1461
- 'const log = message => { try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.appendFileSync(logPath, new Date().toISOString() + " " + message + "\\n", "utf8"); } catch {} };',
1462
- 'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code !== "ESRCH"; } };',
1463
1574
  'const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
1464
- 'const waitForExit = async (pid, timeoutMs) => { const deadline = Date.now() + timeoutMs; while (pid > 1 && Date.now() < deadline) { if (!isAlive(pid)) return true; await sleep(250); } return pid <= 1 || !isAlive(pid); };',
1465
- 'const killProcessTree = async pid => { if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return; if (process.platform === "win32") { spawnSync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); } else { try { process.kill(-pid, "SIGTERM"); } catch { try { process.kill(pid, "SIGTERM"); } catch {} } await sleep(killGraceMs); try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} } } await waitForExit(pid, killGraceMs); };',
1466
- 'const ensureExit = async (pid, label) => { if (pid <= 1 || await waitForExit(pid, 20000)) return; log(label + " pid=" + pid + " did not exit gracefully; terminating its process tree."); await killProcessTree(pid); if (isAlive(pid)) throw new Error(label + " process " + pid + " did not exit before restart."); };',
1575
+ 'const isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1576
+ 'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
1577
+ 'const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));',
1578
+ 'const waitForStateLock = ms => Atomics.wait(lockWaitBuffer, 0, 0, Math.max(1, ms));',
1579
+ 'const removeAbandonedStateLock = () => { let owner = null; try { owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); } catch {} const ownerPid = Number(owner?.pid || 0); if (Number.isInteger(ownerPid) && ownerPid > 1) { if (isAlive(ownerPid)) return false; } else { try { if (Date.now() - fs.statSync(stateLockPath).mtimeMs < 5000) return false; } catch { return true; } } try { fs.rmSync(stateLockPath); return true; } catch { return false; } };',
1580
+ 'const withStateLock = callback => { fs.mkdirSync(path.dirname(stateLockPath), { recursive: true }); const deadline = Date.now() + 10000; let descriptor = null; let token = ""; while (descriptor === null) { try { descriptor = fs.openSync(stateLockPath, "wx", 0o600); token = process.pid + "-" + Date.now() + "-" + Math.random().toString(16).slice(2); fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token, acquiredAt: new Date().toISOString() }), "utf8"); } catch (error) { if (descriptor !== null) { try { fs.closeSync(descriptor); } catch {} descriptor = null; try { fs.rmSync(stateLockPath, { force: true }); } catch {} } if (error?.code !== "EEXIST") throw error; if (removeAbandonedStateLock()) continue; if (Date.now() >= deadline) throw new Error("Timed out waiting for the LiveDesk update state lock: " + stateLockPath); waitForStateLock(Math.min(25, Math.max(1, deadline - Date.now()))); } } try { return callback(); } finally { try { fs.closeSync(descriptor); } catch {} try { const owner = JSON.parse(fs.readFileSync(stateLockPath, "utf8")); if (owner?.token === token && Number(owner?.pid) === process.pid) fs.rmSync(stateLockPath, { force: true }); } catch {} } };',
1581
+ 'const writeFailure = (error, cancelRequested = false) => { try { withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown" || previous.stage === "connected" || previous.stage === "restored") return; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: cancelRequested || previous.cancelRequested === true, error: String(error?.message || error).slice(0, 4000), failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); }); } catch {} process.stderr.write("LiveDesk update handoff failed: " + (error?.message || error) + "\\n"); process.exitCode = 1; };',
1582
+ 'const writeCancellation = error => withStateLock(() => { const previous = readState(); if (previous.operationId && previous.operationId !== operationId) return "superseded"; if (previous.operationId !== operationId) return false; if (previous.stage === "preflight-ready" || previous.stage === "waiting-for-shutdown") return "ready"; if (previous.stage === "failed") return previous.cancelRequested === true ? "cancelled" : "failed"; const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "failed", targetProductVersion, restartVerified: false, cancelRequested: true, error: String(error?.message || error).slice(0, 4000), cancelledAt: now, failedAt: now, updatedAt: now }; const temporary = statePath + "." + process.pid + ".cancel.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); return "cancelled"; });',
1583
+ 'const terminateTree = async child => { const pid = Number(child?.pid || 0); if (pid <= 1) return; if (process.platform === "win32") { await new Promise(resolve => { const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }); let done = false; const finish = () => { if (done) return; done = true; clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { try { killer.kill("SIGKILL"); } catch {} finish(); }, 10000); killer.once("error", finish); killer.once("exit", finish); }); if (isAlive(pid)) { try { child.kill("SIGKILL"); } catch {} } return; } try { process.kill(-pid, "SIGTERM"); } catch { try { child.kill("SIGTERM"); } catch {} } const gracefulDeadline = Date.now() + 2000; while (Date.now() < gracefulDeadline && (isGroupAlive(pid) || isAlive(pid))) await sleep(100); if (isGroupAlive(pid) || isAlive(pid)) { try { process.kill(-pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch {} } } };',
1584
+ 'const writeHandoffStarted = () => withStateLock(() => { if (deadlineExpired()) throw new Error("The absolute LiveDesk Client update deadline expired before handoff."); const previous = readState(); if (previous.operationId !== operationId) throw new Error("LiveDesk update handoff was superseded before exact-package launch."); const now = new Date().toISOString(); const next = { ...previous, operationId, stage: "handoff-started", starterPid: process.pid, updateDeadlineEpochMs, restartVerified: false, error: "", updatedAt: now }; const temporary = statePath + "." + process.pid + ".handoff-started.tmp"; fs.writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 }); fs.renameSync(temporary, statePath); });',
1585
+ 'const monitorPreflight = child => { let settled = false; const finish = (error, preserveFailure = false) => { if (settled) return; settled = true; clearInterval(poll); clearTimeout(timeout); child.unref(); if (preserveFailure) process.exitCode = 1; else if (error) writeFailure(error); }; const inspect = () => { const state = readState(); if (state.operationId !== operationId) return; if (state.stage === "preflight-ready" || state.stage === "waiting-for-shutdown") finish(); else if (state.stage === "failed") finish(null, true); }; const cancelTimedOutHandoff = async () => { const error = new Error(deadlineExpired() ? "The absolute LiveDesk Client update deadline expired before exact-package preflight." : "Timed out waiting for exact-package update preflight."); while (!settled) { inspect(); if (settled) return; let outcome; try { outcome = writeCancellation(error); } catch (lockError) { finish(lockError); return; } if (outcome === "ready" || outcome === "failed") { inspect(); if (settled) return; } else if (outcome === "cancelled" || outcome === "superseded") { await terminateTree(child); finish(null, true); return; } await sleep(25); } }; const poll = setInterval(inspect, 100); const timeoutMs = Math.max(1, Math.min(Math.max(1000, Number(process.env.LIVEDESK_CLIENT_UPDATE_HANDOFF_TIMEOUT_MS || 140000)), updateDeadlineEpochMs - Date.now())); const timeout = setTimeout(() => { void cancelTimedOutHandoff(); }, timeoutMs); child.once("exit", (code, signal) => { inspect(); if (!settled) finish(new Error("Exact-package update supervisor exited before preflight (code=" + (code ?? "none") + ", signal=" + (signal || "none") + ").")); }); inspect(); };',
1586
+ 'const prepareNeutralCwd = () => { if (!neutralCwd) throw new Error("LiveDesk update neutral working directory is unavailable"); fs.mkdirSync(neutralCwd, { recursive: true }); for (let ancestor = neutralCwd; ; ancestor = path.dirname(ancestor)) { const shadow = [path.join(ancestor, "package.json"), path.join(ancestor, "node_modules", "livedesk", "package.json")].find(fs.existsSync); if (shadow) throw new Error("LiveDesk update neutral working directory is shadowed by " + shadow + ": " + neutralCwd); const parent = path.dirname(ancestor); if (parent === ancestor) break; } };',
1587
+ 'if (!operationId || !versionPattern.test(targetProductVersion) || deadlineExpired()) { writeFailure(new Error("Invalid or expired LiveDesk exact-package update handoff."), true); } else { try { prepareNeutralCwd(); writeHandoffStarted();',
1588
+ 'const env = { ...process.env, LIVEDESK_UPDATE_STARTER_PID: String(process.pid) }; const isolated = new Set(["init_cwd", "npm_config_local_prefix", "npm_config_workspace", "npm_config_workspaces", "npm_config_include_workspace_root", "npm_package_json", "npm_lifecycle_event", "npm_lifecycle_script"]); for (const key of Object.keys(env)) if (isolated.has(key.toLowerCase())) delete env[key]; env.INIT_CWD = neutralCwd; env.npm_config_local_prefix = neutralCwd; env.npm_config_include_workspace_root = "false";',
1467
1589
  'const nodeDir = path.dirname(process.execPath);',
1468
- 'const npxCandidates = process.platform === "win32" ? [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeDir, "npx.cmd"), path.join(nodeDir, "npx.exe"), "npx.cmd"] : [process.env.LIVEDESK_NPX_EXECUTABLE, path.join(nodeDir, "npx"), "npx"];',
1469
- 'const npx = npxCandidates.find(candidate => candidate && (!path.isAbsolute(candidate) || fs.existsSync(candidate)));',
1470
- 'if (!npx) { writeState("failed", { error: "LiveDesk npx executable was not found." }); throw new Error("LiveDesk npx executable was not found."); }',
1471
- 'const npmExecPath = String(process.env.npm_execpath || process.env.NPM_EXECPATH || "").trim();',
1472
- 'const npxCliCandidates = [process.env.LIVEDESK_NPX_CLI_PATH, npmExecPath ? path.join(path.dirname(npmExecPath), "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].filter(Boolean);',
1473
- 'const npxCli = npxCliCandidates.find(candidate => fs.existsSync(candidate));',
1590
+ 'const npxCli = [env.LIVEDESK_NPX_CLI_PATH, env.npm_execpath ? path.join(path.dirname(env.npm_execpath), "npx-cli.js") : "", env.LIVEDESK_NPX_EXECUTABLE ? path.join(path.dirname(env.LIVEDESK_NPX_EXECUTABLE), "node_modules", "npm", "bin", "npx-cli.js") : "", path.join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js")].find(value => value && fs.existsSync(value));',
1591
+ 'const npx = env.LIVEDESK_NPX_EXECUTABLE || (process.platform === "win32" ? "npx.cmd" : "npx");',
1592
+ 'const args = ["-y", "--prefer-online", "livedesk@" + targetProductVersion, "--internal-legacy-client-update"];',
1474
1593
  'const quoteCmd = value => "\\"" + String(value).replaceAll("%", "%%").replaceAll("\\"", "\\"\\"") + "\\"";',
1475
- 'const invocation = npxArgs => npxCli ? { command: process.execPath, args: [npxCli, ...npxArgs] } : process.platform === "win32" ? { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + npxArgs.map(quoteCmd).join(" ")] } : { command: npx, args: npxArgs };',
1476
- 'const runToExit = (npxArgs, timeoutMs) => new Promise((resolve, reject) => { const call = invocation(npxArgs); const child = spawn(call.command, call.args, { env: process.env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; let settled = false; child.stdout?.on("data", chunk => { stdout = (stdout + chunk.toString()).slice(-65536); }); child.stderr?.on("data", chunk => { stderr = (stderr + chunk.toString()).slice(-65536); }); const timer = setTimeout(() => { if (settled) return; settled = true; void killProcessTree(child.pid || 0).finally(() => reject(new Error("LiveDesk package preparation timed out."))); }, timeoutMs); child.once("error", error => { if (settled) return; settled = true; clearTimeout(timer); reject(error); }); child.once("exit", (code, signal) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ code: code ?? -1, signal, stdout, stderr }); }); });',
1477
- 'const attempts = [];',
1478
- 'const attemptIdFor = (kind, number) => operationId + "-" + kind + "-" + number + "-" + crypto.randomBytes(4).toString("hex");',
1479
- 'const updateAttempt = (attempt, patch) => { Object.assign(attempt, patch, { updatedAt: new Date().toISOString() }); return attempts.map(item => ({ ...item })); };',
1480
- 'const killAttempt = async (restartPid, attemptId) => { const state = readState(); const pids = new Set([Number(restartPid || 0)]); if (state.operationId === operationId && state.attemptId === attemptId) { pids.add(Number(state.launcherPid || 0)); pids.add(Number(state.agentPid || 0)); } for (const pid of pids) await killProcessTree(pid); };',
1481
- 'const launchAndVerify = async ({ kind, number, expectedProductVersion, expectedAgentVersion, call }) => { const attemptId = attemptIdFor(kind, number); const attempt = { attemptId, kind, number, status: "launching", expectedProductVersion, expectedAgentVersion, startedAt: new Date().toISOString() }; attempts.push(attempt); const attemptEnv = { ...process.env, LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID: attemptId, LIVEDESK_CLIENT_UPDATE_ATTEMPT_KIND: kind, LIVEDESK_CLIENT_UPDATE_ATTEMPT_NUMBER: String(number), LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId, LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion, LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion, LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion }; const logFd = fs.openSync(logPath, "a"); let child; try { child = spawn(call.command, call.args, { cwd: process.env.LIVEDESK_CLIENT_UPDATE_CWD || process.cwd(), env: attemptEnv, detached: true, windowsHide: true, stdio: ["ignore", logFd, logFd] }); await new Promise((resolve, reject) => { child.once("error", reject); child.once("spawn", resolve); }); } finally { fs.closeSync(logFd); } const restartPid = Number(child?.pid || 0); updateAttempt(attempt, { status: "waiting-for-connected-proof", restartPid }); writeState(kind + "-launched", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: "" }); let exitResult = null; child.once("exit", (code, signal) => { exitResult = { code, signal }; }); const deadline = Date.now() + proofTimeoutMs; let proof = null; let failure = ""; while (Date.now() < deadline) { const state = readState(); if (state.operationId === operationId && state.attemptId === attemptId && state.stage === "connected") { const observedProduct = cleanVersion(state.productVersion); const observedAgent = cleanVersion(state.agentVersion); if (observedProduct === expectedProductVersion && observedAgent === expectedAgentVersion) { proof = state; break; } failure = "Connected version mismatch expected product " + expectedProductVersion + " / Agent " + expectedAgentVersion + ", observed product " + (observedProduct || "unknown") + " / Agent " + (observedAgent || "unknown") + "."; break; } if (exitResult) { failure = "Launcher exited before exact connected proof (code=" + (exitResult.code ?? "none") + ", signal=" + (exitResult.signal || "none") + ")."; break; } await sleep(proofPollMs); } if (proof) { updateAttempt(attempt, { status: "connected", connectedAt: proof.connectedAt || new Date().toISOString(), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion) }); if (child && !exitResult) child.unref(); writeState(kind === "target" ? "connected" : "restored", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, launcherPid: Number(proof.launcherPid || 0), agentPid: Number(proof.agentPid || 0), productVersion: cleanVersion(proof.productVersion), agentVersion: cleanVersion(proof.agentVersion), attempts: attempts.map(item => ({ ...item })), restartVerified: kind === "target", restored: kind === "restore", connectedAt: proof.connectedAt || new Date().toISOString(), error: kind === "restore" ? "Target update failed; previous local Client was restored." : "" }); return true; } failure = failure || "Timed out waiting for exact connected proof for " + attemptId + "."; await killAttempt(restartPid, attemptId); updateAttempt(attempt, { status: "failed", failedAt: new Date().toISOString(), killed: true, error: failure }); writeState(kind + "-attempt-failed", { attemptId, attemptKind: kind, attemptNumber: number, restartPid, attempts: attempts.map(item => ({ ...item })), restartVerified: false, error: failure }); log(attemptId + " failed: " + failure); return false; };',
1482
- 'process.env.LIVEDESK_SKIP_BROWSER_OPEN = "1";',
1483
- '(async () => { writeState("preparing-package", { packageSpec, attempts: [], error: "" }); log("preparing " + packageSpec); const prepared = await runToExit(["-y", "--prefer-online", packageSpec, "--version"], 120000); if (prepared.code !== 0) throw new Error("LiveDesk package preparation failed (exit=" + prepared.code + "): " + (prepared.stderr || prepared.stdout).trim().slice(-2000)); const stdoutVersions = prepared.stdout.split(/\\r?\\n/).map(line => line.trim()).filter(line => versionPattern.test(line)); if (!stdoutVersions.includes(targetProductVersion)) throw new Error("Prepared LiveDesk stdout did not report requested product version " + targetProductVersion + ". stdout=" + prepared.stdout.trim().slice(-1000)); writeState("waiting-for-shutdown", { observedVersion: targetProductVersion, observedProductVersion: targetProductVersion, preparedAt: new Date().toISOString(), attempts: [], error: "" }); log("package ready product=" + targetProductVersion + "; waiting for current runtime shutdown"); await ensureExit(agentPid, "Agent"); await ensureExit(waitPid, "Launcher"); const roleArgs = ["--force-role", "client", "--no-login", "--no-open", ...preservedArgs]; const targetCall = invocation(["-y", "--prefer-online", packageSpec, ...roleArgs]); for (let number = 1; number <= targetLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "target", number, expectedProductVersion: targetProductVersion, expectedAgentVersion: targetAgentVersion, call: targetCall })) { log("target connected with exact product " + targetProductVersion + " / Agent " + targetAgentVersion); return; } } writeState("restoring", { attempts: attempts.map(item => ({ ...item })), error: "All target launch attempts failed; restoring previous local Client." }); const restoreCall = localLauncher && fs.existsSync(localLauncher) ? { command: process.execPath, args: [localLauncher, ...roleArgs] } : invocation(["-y", "--prefer-offline", "livedesk@" + currentProductVersion, ...roleArgs]); for (let number = 1; number <= restoreLaunchLimit; number += 1) { if (await launchAndVerify({ kind: "restore", number, expectedProductVersion: currentProductVersion, expectedAgentVersion: currentAgentVersion, call: restoreCall })) { log("previous local Client restored at product " + currentProductVersion + " / Agent " + currentAgentVersion); process.exitCode = 1; return; } } throw new Error("Target update failed after 3 attempts and previous local Client restoration failed after 2 attempts."); })().catch(error => { const message = error?.stack || error?.message || String(error); log("failed: " + message); try { writeState("failed", { attempts: attempts.map(item => ({ ...item })), error: String(error?.message || error).slice(0, 4000), failedAt: new Date().toISOString(), restartVerified: false }); } catch {} process.exitCode = 1; });',
1594
+ 'const invocation = npxCli ? { command: process.execPath, args: [npxCli, ...args] } : process.platform === "win32" ? { command: env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", "call " + quoteCmd(npx) + " " + args.map(quoteCmd).join(" ")] } : { command: npx, args };',
1595
+ 'if (!npxCli && path.isAbsolute(npx) && !fs.existsSync(npx)) { writeFailure(new Error("LiveDesk npx executable was not found: " + npx)); } else { try { const child = spawn(invocation.command, invocation.args, { cwd: neutralCwd, env, detached: true, stdio: "ignore", windowsHide: true }); child.once("error", writeFailure); child.once("spawn", () => monitorPreflight(child)); } catch (error) { writeFailure(error); } }',
1596
+ '} catch (error) { writeFailure(error, deadlineExpired()); } }',
1484
1597
  ''
1485
1598
  ].join('\n');
1486
1599
  }
@@ -1490,6 +1603,7 @@ async function scheduleClientUpdate(options, payload = {}) {
1490
1603
  const statePath = getClientUpdateStatePath();
1491
1604
  let targetProductVersion = '';
1492
1605
  let targetAgentVersion = '';
1606
+ let updateDeadlineEpochMs = 0;
1493
1607
  try {
1494
1608
  targetProductVersion = normalizeClientUpdateTargetVersion(
1495
1609
  payload.targetProductVersion
@@ -1509,11 +1623,22 @@ async function scheduleClientUpdate(options, payload = {}) {
1509
1623
  || targetProductVersion
1510
1624
  || ''
1511
1625
  );
1626
+ const updateTimeoutMs = Number(payload.updateTimeoutMs);
1627
+ if (!Number.isSafeInteger(updateTimeoutMs)
1628
+ || updateTimeoutMs < 1_000
1629
+ || updateTimeoutMs > 90 * 60_000) {
1630
+ throw new Error('LiveDesk Client update requires a bounded deadline budget.');
1631
+ }
1632
+ updateDeadlineEpochMs = Date.now() + updateTimeoutMs;
1633
+ const originalCwd = path.resolve(process.cwd());
1634
+ const neutralCwd = await prepareClientUpdateNeutralCwd(operationId);
1512
1635
  await writeClientUpdateState('scheduled', {
1513
1636
  operationId,
1514
1637
  targetVersion: targetAgentVersion,
1515
1638
  targetProductVersion,
1516
1639
  targetAgentVersion,
1640
+ updateTimeoutMs,
1641
+ updateDeadlineEpochMs,
1517
1642
  requestedAt: new Date().toISOString(),
1518
1643
  restartVerified: false,
1519
1644
  error: ''
@@ -1533,29 +1658,41 @@ async function scheduleClientUpdate(options, payload = {}) {
1533
1658
  if (!command) {
1534
1659
  throw new Error('LiveDesk npx executable was not found.');
1535
1660
  }
1661
+ const frozenCommand = path.isAbsolute(command) || !/[\\/]/.test(command)
1662
+ ? command
1663
+ : path.resolve(originalCwd, command);
1664
+ const configuredLocalLauncher = String(
1665
+ process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1666
+ || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1667
+ || ''
1668
+ ).trim();
1669
+ const localLauncher = configuredLocalLauncher
1670
+ ? path.resolve(originalCwd, configuredLocalLauncher)
1671
+ : '';
1672
+ const updateEnvironment = buildClientUpdateNpxEnvironment({
1673
+ ...process.env,
1674
+ LIVEDESK_NODE_EXECUTABLE: process.execPath,
1675
+ LIVEDESK_NPX_EXECUTABLE: frozenCommand,
1676
+ LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1677
+ LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1678
+ LIVEDESK_SKIP_BROWSER_OPEN: '1',
1679
+ LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1680
+ LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1681
+ LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1682
+ LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1683
+ LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1684
+ LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: localLauncher,
1685
+ LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1686
+ LIVEDESK_CLIENT_UPDATE_DEADLINE_EPOCH_MS: String(updateDeadlineEpochMs),
1687
+ LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1688
+ LIVEDESK_UPDATE_NEUTRAL_CWD: neutralCwd,
1689
+ LIVEDESK_UPDATE_ORIGINAL_CWD: originalCwd,
1690
+ LIVEDESK_CLIENT_UPDATE_CWD: originalCwd
1691
+ }, neutralCwd);
1536
1692
  return await new Promise((resolve, reject) => {
1537
1693
  const child = spawn(process.execPath, ['-e', buildClientUpdateBootstrapScript()], {
1538
- env: {
1539
- ...process.env,
1540
- LIVEDESK_NODE_EXECUTABLE: process.execPath,
1541
- LIVEDESK_NPX_EXECUTABLE: command,
1542
- LIVEDESK_UPDATE_WAIT_PID: String(parentPid),
1543
- LIVEDESK_UPDATE_AGENT_PID: String(process.pid),
1544
- LIVEDESK_SKIP_BROWSER_OPEN: '1',
1545
- LIVEDESK_CLIENT_UPDATE_TARGET_VERSION: targetAgentVersion,
1546
- LIVEDESK_CLIENT_UPDATE_TARGET_PRODUCT_VERSION: targetProductVersion,
1547
- LIVEDESK_CLIENT_UPDATE_TARGET_AGENT_VERSION: targetAgentVersion,
1548
- LIVEDESK_CLIENT_UPDATE_CURRENT_PRODUCT_VERSION: PRODUCT_VERSION,
1549
- LIVEDESK_CLIENT_UPDATE_CURRENT_AGENT_VERSION: AGENT_VERSION,
1550
- LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER: String(
1551
- process.env.LIVEDESK_UNIFIED_LAUNCHER_ENTRY
1552
- || process.env.LIVEDESK_CLIENT_UPDATE_LOCAL_LAUNCHER
1553
- || ''
1554
- ),
1555
- LIVEDESK_CLIENT_UPDATE_OPERATION_ID: operationId,
1556
- LIVEDESK_CLIENT_UPDATE_STATE_PATH: statePath,
1557
- LIVEDESK_CLIENT_UPDATE_CWD: process.cwd()
1558
- },
1694
+ cwd: neutralCwd,
1695
+ env: updateEnvironment,
1559
1696
  detached: true,
1560
1697
  stdio: 'ignore',
1561
1698
  windowsHide: true
@@ -1570,6 +1707,7 @@ async function scheduleClientUpdate(options, payload = {}) {
1570
1707
  targetProductVersion,
1571
1708
  targetAgentVersion,
1572
1709
  operationId,
1710
+ updateDeadlineEpochMs,
1573
1711
  statePath,
1574
1712
  parentPid,
1575
1713
  restartAfterMs: 0
@@ -1623,8 +1761,27 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
1623
1761
  commandId: message.commandId,
1624
1762
  result
1625
1763
  });
1626
- if (await waitForClientUpdateShutdownSignal(result.operationId)) {
1627
- process.exit(EXIT_CLIENT_UPDATE);
1764
+ const shutdownSignal = await waitForClientUpdateShutdownSignal(
1765
+ result.operationId,
1766
+ result.updateDeadlineEpochMs
1767
+ );
1768
+ if (shutdownSignal.ready) {
1769
+ // The exact package supervisor is the sole owner of old-runtime
1770
+ // termination, so a deadline can never race a voluntary exit.
1771
+ return;
1772
+ } else {
1773
+ writeJsonLine(socket, {
1774
+ type: 'command.result',
1775
+ commandId: message.commandId,
1776
+ error: shutdownSignal.error,
1777
+ result: {
1778
+ ok: false,
1779
+ status: 'failed',
1780
+ error: shutdownSignal.error,
1781
+ operationId: result.operationId,
1782
+ completedAt: new Date().toISOString()
1783
+ }
1784
+ });
1628
1785
  }
1629
1786
  } catch (error) {
1630
1787
  writeJsonLine(socket, {
@@ -603,8 +603,15 @@ function readClientUpdateState() {
603
603
  function writeClientUpdateRuntimeStage(stage, details = {}) {
604
604
  const operationId = String(process.env.LIVEDESK_CLIENT_UPDATE_OPERATION_ID || '').trim();
605
605
  if (!operationId) return;
606
+ const attemptId = String(process.env.LIVEDESK_CLIENT_UPDATE_ATTEMPT_ID || '').trim();
606
607
  const previous = readClientUpdateState();
607
608
  if (previous?.operationId && previous.operationId !== operationId) return;
609
+ if (previous?.attemptId && attemptId && previous.attemptId !== attemptId) return;
610
+ if (previous?.stage === 'connected'
611
+ && previous?.restartVerified === true
612
+ && stage !== 'connected') {
613
+ return;
614
+ }
608
615
  const targetVersion = String(process.env.LIVEDESK_CLIENT_UPDATE_TARGET_VERSION || '').trim().replace(/^v/i, '');
609
616
  const productVersion = String(process.env.LIVEDESK_NPM_LAUNCHER_VERSION || readPackageVersion()).trim();
610
617
  const state = {
@@ -615,6 +622,7 @@ function writeClientUpdateRuntimeStage(stage, details = {}) {
615
622
  productVersion,
616
623
  agentVersion: readPackageVersion(),
617
624
  launcherPid: process.pid,
625
+ attemptId,
618
626
  updatedAt: new Date().toISOString(),
619
627
  ...details
620
628
  };
@@ -3592,9 +3600,10 @@ function buildClientAgentEnvironment(prepared) {
3592
3600
  env.LIVEDESK_NPX_CLI_PATH = npxCliPath;
3593
3601
  }
3594
3602
  env.LIVEDESK_CLIENT_MANAGER = String(prepared?.manager || env.LIVEDESK_CLIENT_MANAGER || '');
3595
- env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
3596
- env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
3603
+ env.LIVEDESK_CLIENT_PAIR_TOKEN = String(prepared?.pair || env.LIVEDESK_CLIENT_PAIR_TOKEN || '');
3604
+ env.LIVEDESK_CLIENT_NAME = String(prepared?.name || env.LIVEDESK_CLIENT_NAME || '');
3597
3605
  env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3606
+ env.LIVEDESK_DEVICE_ID = String(prepared?.deviceId || env.LIVEDESK_DEVICE_ID || '');
3598
3607
  env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3599
3608
  env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
3600
3609
  env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
@@ -3805,16 +3814,19 @@ function resolveFastLaunch(runtime) {
3805
3814
  }
3806
3815
 
3807
3816
  function spawnAgent(command, args, env = process.env, onStart = null) {
3808
- const child = spawn(command, args, {
3809
- env,
3810
- stdio: 'inherit',
3811
- windowsHide: true
3812
- });
3813
- activeAgentProcess = child;
3814
- writeClientUpdateRuntimeStage('agent-started', {
3815
- agentPid: Number(child.pid || 0),
3817
+ // Publish only a pre-spawn stage. A very fast child can reach Hub welcome
3818
+ // before spawn() returns; a post-spawn write would then erase its one-shot
3819
+ // connected proof.
3820
+ writeClientUpdateRuntimeStage('agent-launching', {
3821
+ agentPid: 0,
3816
3822
  restartVerified: false
3817
3823
  });
3824
+ const child = spawn(command, args, {
3825
+ env,
3826
+ stdio: 'inherit',
3827
+ windowsHide: true
3828
+ });
3829
+ activeAgentProcess = child;
3818
3830
  if (typeof onStart === 'function') {
3819
3831
  onStart(child);
3820
3832
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.170",
3
+ "version": "0.1.172",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,10 +39,10 @@
39
39
  "ws": "^8.18.3"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@livedesk/fast-linux-x64": "0.1.374",
43
- "@livedesk/fast-osx-arm64": "0.1.376",
44
- "@livedesk/fast-osx-x64": "0.1.376",
45
- "@livedesk/fast-win-x64": "0.1.374"
42
+ "@livedesk/fast-linux-x64": "0.1.378",
43
+ "@livedesk/fast-osx-arm64": "0.1.378",
44
+ "@livedesk/fast-osx-x64": "0.1.378",
45
+ "@livedesk/fast-win-x64": "0.1.378"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"