@msvesper/vesper-link 0.1.5 → 0.1.6
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/dist/installer.d.ts +1 -0
- package/dist/installer.js +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.js +226 -51
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/installer.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface InstallerDependencies {
|
|
|
39
39
|
requiredCompanion?: ManagedRuntime['companion'];
|
|
40
40
|
bootstrapAuthKey?: string;
|
|
41
41
|
failIfLoginRequired?: boolean;
|
|
42
|
+
replaceExistingTransport?: boolean;
|
|
42
43
|
}): Promise<RemoteTransport>;
|
|
43
44
|
getHealth: typeof getHealth;
|
|
44
45
|
getCapabilities: typeof getCapabilities;
|
package/dist/installer.js
CHANGED
|
@@ -85,6 +85,7 @@ export async function installVesperLink(options) {
|
|
|
85
85
|
requiredCompanion: runtime.companion,
|
|
86
86
|
bootstrapAuthKey,
|
|
87
87
|
failIfLoginRequired: options.requireExistingIdentity,
|
|
88
|
+
replaceExistingTransport: options.requireExistingIdentity,
|
|
88
89
|
onNeedsLogin: (url) => {
|
|
89
90
|
options.onNeedsLogin?.(url);
|
|
90
91
|
},
|
package/dist/transport.d.ts
CHANGED
|
@@ -21,8 +21,13 @@ export interface AcquireTransportOptions {
|
|
|
21
21
|
spawnChild?: (executable: string, args: string[], env: NodeJS.ProcessEnv) => ChildProcessWithoutNullStreams;
|
|
22
22
|
onNeedsLogin?: (url: string) => void;
|
|
23
23
|
failIfLoginRequired?: boolean;
|
|
24
|
+
replaceExistingTransport?: boolean;
|
|
24
25
|
bootstrapAuthKey?: string;
|
|
25
26
|
startTimeoutMs?: number;
|
|
27
|
+
handoffTimeoutMs?: number;
|
|
28
|
+
terminateProcess?: (pid: number) => void;
|
|
29
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
30
|
+
restartPriorTransport?: (executable: string, args: string[], env: NodeJS.ProcessEnv) => void;
|
|
26
31
|
}
|
|
27
32
|
export declare function consumeBootstrapAuthKey(explicitAuthKey: string | undefined, environment?: NodeJS.ProcessEnv): string | undefined;
|
|
28
33
|
export declare function acquireRemoteTransport(options: AcquireTransportOptions): Promise<RemoteTransport>;
|
package/dist/transport.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { isIPv4 } from 'node:net';
|
|
4
4
|
import * as http from 'node:http';
|
|
5
|
+
import * as path from 'node:path';
|
|
5
6
|
import { Readable } from 'node:stream';
|
|
6
7
|
import { assertCompanionProtocol, VESPER_NET_PROTOCOL_VERSION, resolveCompanion } from './companion.js';
|
|
7
8
|
const START_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -9,7 +10,9 @@ const ATTACH_TIMEOUT_MS = 2_000;
|
|
|
9
10
|
const POLL_INTERVAL_MS = 100;
|
|
10
11
|
const MAX_CONTROL_BYTES = 64 * 1024;
|
|
11
12
|
const LOCAL_SECRET_HEADER = 'x-vesper-transport-secret';
|
|
13
|
+
const EXPECTED_OWNER_PID_HEADER = 'x-vesper-expected-owner-pid';
|
|
12
14
|
const KEEPALIVE_MS = 10_000;
|
|
15
|
+
const HANDOFF_TIMEOUT_MS = 10_000;
|
|
13
16
|
const PROXY_ENVIRONMENT_KEYS = new Set([
|
|
14
17
|
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
15
18
|
]);
|
|
@@ -17,6 +20,8 @@ const AUTH_KEY_ENVIRONMENT_KEYS = new Set([
|
|
|
17
20
|
'vesper_ts_authkey', 'ts_authkey', 'ts_auth_key',
|
|
18
21
|
]);
|
|
19
22
|
const AUTH_KEY_PATTERN = /^tskey-auth-[A-Za-z0-9_-]{16,512}$/;
|
|
23
|
+
const TRANSPORT_SECRET_ENVIRONMENT_KEY = 'VESPER_TRANSPORT_SECRET';
|
|
24
|
+
const TRANSPORT_SECRET_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
20
25
|
function directLoopbackFetch(input, init = {}) {
|
|
21
26
|
const inputRequest = input instanceof Request ? input : undefined;
|
|
22
27
|
const url = new URL(inputRequest?.url ?? input.toString());
|
|
@@ -140,7 +145,16 @@ async function readRendezvous(filePath) {
|
|
|
140
145
|
throw error;
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
|
-
|
|
148
|
+
function validateLease(candidate, hostUrl) {
|
|
149
|
+
if (candidate.protocolVersion !== VESPER_NET_PROTOCOL_VERSION || candidate.hostUrl !== hostUrl
|
|
150
|
+
|| typeof candidate.tailnetName !== 'string' || !candidate.tailnetName
|
|
151
|
+
|| typeof candidate.nodeName !== 'string' || !candidate.nodeName
|
|
152
|
+
|| typeof candidate.ipv4 !== 'string' || !isIPv4(candidate.ipv4)
|
|
153
|
+
|| typeof candidate.executablePath !== 'string' || !candidate.executablePath) {
|
|
154
|
+
throw new Error('Running vesper-net transport targets a different host or protocol');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function inspectTransport(paths, hostUrl, fetchImpl, renewLease) {
|
|
144
158
|
let rendezvous;
|
|
145
159
|
try {
|
|
146
160
|
rendezvous = await readRendezvous(paths.transportRendezvousPath);
|
|
@@ -154,54 +168,49 @@ async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
|
154
168
|
if (!rendezvous || !isAlive(rendezvous.pid)) {
|
|
155
169
|
return null;
|
|
156
170
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const readLease = async (inspectOnly) => {
|
|
170
|
-
const response = await requestLease(inspectOnly);
|
|
171
|
-
if (!response?.ok) {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
return await response.json();
|
|
175
|
-
};
|
|
176
|
-
let lease = await readLease(requiredExecutablePath !== undefined);
|
|
177
|
-
if (!lease) {
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = await fetchImpl(`${rendezvous.endpoint}/__vesper_transport/${renewLease ? 'lease' : 'identity'}`, {
|
|
174
|
+
headers: { [LOCAL_SECRET_HEADER]: rendezvous.secret },
|
|
175
|
+
signal: AbortSignal.timeout(ATTACH_TIMEOUT_MS),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
178
179
|
return null;
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const lease = await response.json();
|
|
185
|
+
validateLease(lease, hostUrl);
|
|
186
|
+
return { rendezvous, lease };
|
|
187
|
+
}
|
|
188
|
+
function sameExecutable(left, right) {
|
|
189
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
190
|
+
}
|
|
191
|
+
async function canonicalExecutable(executable) {
|
|
192
|
+
return fs.realpath(executable).catch(() => path.resolve(executable));
|
|
193
|
+
}
|
|
194
|
+
async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
195
|
+
let inspected = await inspectTransport(paths, hostUrl, fetchImpl, requiredExecutablePath === undefined);
|
|
196
|
+
if (!inspected) {
|
|
197
|
+
return null;
|
|
188
198
|
}
|
|
189
|
-
validateLease(lease);
|
|
190
199
|
if (requiredExecutablePath) {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (!
|
|
200
|
+
const actualExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
201
|
+
if (!sameExecutable(actualExecutablePath, requiredExecutablePath)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
inspected = await inspectTransport(paths, hostUrl, fetchImpl, true);
|
|
205
|
+
if (!inspected) {
|
|
197
206
|
return null;
|
|
198
207
|
}
|
|
199
|
-
|
|
200
|
-
if (!
|
|
208
|
+
const renewedExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
209
|
+
if (!sameExecutable(renewedExecutablePath, requiredExecutablePath)) {
|
|
201
210
|
return null;
|
|
202
211
|
}
|
|
203
|
-
validateLease(lease);
|
|
204
212
|
}
|
|
213
|
+
const { rendezvous, lease } = inspected;
|
|
205
214
|
const transportFetch = async (input, init) => {
|
|
206
215
|
const remote = input instanceof Request ? new URL(input.url) : new URL(input.toString());
|
|
207
216
|
if (remote.origin !== new URL(hostUrl).origin) {
|
|
@@ -238,6 +247,122 @@ async function attach(paths, hostUrl, fetchImpl, requiredExecutablePath) {
|
|
|
238
247
|
},
|
|
239
248
|
};
|
|
240
249
|
}
|
|
250
|
+
function isManagedRuntimeExecutable(paths, executable) {
|
|
251
|
+
const relative = path.relative(path.resolve(paths.runtimeDir), path.resolve(executable));
|
|
252
|
+
const executableName = path.basename(executable).toLowerCase();
|
|
253
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
254
|
+
&& (executableName === 'vesper-net' || executableName === 'vesper-net.exe')
|
|
255
|
+
&& relative.split(path.sep).includes('node_modules');
|
|
256
|
+
}
|
|
257
|
+
async function prepareTransportHandoff(options, fetchImpl, requiredExecutablePath) {
|
|
258
|
+
const inspected = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
259
|
+
if (!inspected) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
const oldExecutablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
263
|
+
if (sameExecutable(oldExecutablePath, requiredExecutablePath)) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (!isManagedRuntimeExecutable(options.paths, oldExecutablePath)) {
|
|
267
|
+
throw new Error(`Refusing to replace vesper-net transport outside the managed runtime: ${oldExecutablePath}`);
|
|
268
|
+
}
|
|
269
|
+
return { inspected, oldExecutablePath };
|
|
270
|
+
}
|
|
271
|
+
async function stopTransportForHandoff(options, fetchImpl, handoff) {
|
|
272
|
+
const { inspected, oldExecutablePath } = handoff;
|
|
273
|
+
const oldPid = inspected.rendezvous.pid;
|
|
274
|
+
const processIsAlive = options.isProcessAlive ?? isAlive;
|
|
275
|
+
if (inspected.lease.supportsShutdown === true) {
|
|
276
|
+
let response;
|
|
277
|
+
try {
|
|
278
|
+
response = await fetchImpl(`${inspected.rendezvous.endpoint}/__vesper_transport/shutdown`, {
|
|
279
|
+
method: 'POST', headers: {
|
|
280
|
+
[LOCAL_SECRET_HEADER]: inspected.rendezvous.secret,
|
|
281
|
+
[EXPECTED_OWNER_PID_HEADER]: String(oldPid),
|
|
282
|
+
},
|
|
283
|
+
signal: AbortSignal.timeout(ATTACH_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
throw new Error(`Failed to gracefully stop prior vesper-net transport (pid=${oldPid})`);
|
|
288
|
+
}
|
|
289
|
+
if (response.status === 409) {
|
|
290
|
+
// The inspected owner has already yielded the inherited listener.
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`Failed to gracefully stop prior vesper-net transport (pid=${oldPid})`);
|
|
295
|
+
}
|
|
296
|
+
if (await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
300
|
+
}
|
|
301
|
+
const revalidated = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
302
|
+
if (!revalidated) {
|
|
303
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
304
|
+
}
|
|
305
|
+
const revalidatedExecutablePath = await canonicalExecutable(revalidated.lease.executablePath);
|
|
306
|
+
if (revalidated.rendezvous.pid !== oldPid
|
|
307
|
+
|| revalidated.rendezvous.endpoint !== inspected.rendezvous.endpoint
|
|
308
|
+
|| revalidated.rendezvous.secret !== inspected.rendezvous.secret
|
|
309
|
+
|| !sameExecutable(revalidatedExecutablePath, oldExecutablePath)
|
|
310
|
+
|| !isManagedRuntimeExecutable(options.paths, revalidatedExecutablePath)) {
|
|
311
|
+
throw new Error('vesper-net transport changed ownership during upgrade handoff');
|
|
312
|
+
}
|
|
313
|
+
(options.terminateProcess ?? ((pid) => process.kill(pid, 'SIGTERM')))(oldPid);
|
|
314
|
+
if (!await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
315
|
+
throw new Error(`Timed out stopping prior vesper-net transport (pid=${oldPid})`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function evictUnexpectedTransportOwner(options, fetchImpl, requiredExecutablePath) {
|
|
319
|
+
const inspected = await inspectTransport(options.paths, options.hostUrl, fetchImpl, false);
|
|
320
|
+
if (!inspected) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const executablePath = await canonicalExecutable(inspected.lease.executablePath);
|
|
324
|
+
if (sameExecutable(executablePath, requiredExecutablePath)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!isManagedRuntimeExecutable(options.paths, executablePath)) {
|
|
328
|
+
throw new Error(`Refusing to replace vesper-net transport outside the managed runtime: ${executablePath}`);
|
|
329
|
+
}
|
|
330
|
+
await stopTransportForHandoff(options, fetchImpl, { inspected, oldExecutablePath: executablePath });
|
|
331
|
+
}
|
|
332
|
+
async function waitForProcessExit(pid, timeoutMs, processIsAlive) {
|
|
333
|
+
const deadline = Date.now() + timeoutMs;
|
|
334
|
+
while (Date.now() < deadline) {
|
|
335
|
+
if (!processIsAlive(pid)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
339
|
+
}
|
|
340
|
+
return !processIsAlive(pid);
|
|
341
|
+
}
|
|
342
|
+
function defaultRestartPriorTransport(executable, args, env) {
|
|
343
|
+
const child = spawn(executable, args, { env, detached: true, windowsHide: true, stdio: 'ignore' });
|
|
344
|
+
child.unref();
|
|
345
|
+
}
|
|
346
|
+
async function restartPriorTransport(options, handoff, args, error) {
|
|
347
|
+
if (handoff) {
|
|
348
|
+
try {
|
|
349
|
+
const oldPid = handoff.inspected.rendezvous.pid;
|
|
350
|
+
const processIsAlive = options.isProcessAlive ?? isAlive;
|
|
351
|
+
if (processIsAlive(oldPid) && !await waitForProcessExit(oldPid, options.handoffTimeoutMs ?? HANDOFF_TIMEOUT_MS, processIsAlive)) {
|
|
352
|
+
throw new Error(`Prior vesper-net transport is still exiting (pid=${oldPid})`);
|
|
353
|
+
}
|
|
354
|
+
(options.restartPriorTransport ?? defaultRestartPriorTransport)(handoff.oldExecutablePath, args, companionEnvironment(process.env));
|
|
355
|
+
}
|
|
356
|
+
catch (recoveryError) {
|
|
357
|
+
throw new AggregateError([error, recoveryError], 'vesper-net candidate failed and prior transport restart failed');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
async function failAfterHandoff(options, handoff, baseArgs, child, error) {
|
|
363
|
+
child.kill('SIGTERM');
|
|
364
|
+
return restartPriorTransport(options, handoff, baseArgs, error);
|
|
365
|
+
}
|
|
241
366
|
function defaultSpawn(executable, args, env) {
|
|
242
367
|
return spawn(executable, args, {
|
|
243
368
|
env, detached: true, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -246,7 +371,8 @@ function defaultSpawn(executable, args, env) {
|
|
|
246
371
|
function companionEnvironment(environment, bootstrapAuthKey) {
|
|
247
372
|
const filtered = Object.fromEntries(Object.entries(environment).filter(([key]) => {
|
|
248
373
|
const normalized = key.toLowerCase();
|
|
249
|
-
return !PROXY_ENVIRONMENT_KEYS.has(normalized) && !AUTH_KEY_ENVIRONMENT_KEYS.has(normalized)
|
|
374
|
+
return !PROXY_ENVIRONMENT_KEYS.has(normalized) && !AUTH_KEY_ENVIRONMENT_KEYS.has(normalized)
|
|
375
|
+
&& normalized !== TRANSPORT_SECRET_ENVIRONMENT_KEY.toLowerCase();
|
|
250
376
|
}));
|
|
251
377
|
if (bootstrapAuthKey !== undefined) {
|
|
252
378
|
filtered.VESPER_TS_AUTHKEY = bootstrapAuthKey;
|
|
@@ -292,7 +418,7 @@ export async function acquireRemoteTransport(options) {
|
|
|
292
418
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
293
419
|
}
|
|
294
420
|
const requiredExecutablePath = companion
|
|
295
|
-
? await
|
|
421
|
+
? await canonicalExecutable(companion.executable)
|
|
296
422
|
: undefined;
|
|
297
423
|
const existing = await attach(options.paths, options.hostUrl, fetchImpl, requiredExecutablePath);
|
|
298
424
|
if (existing) {
|
|
@@ -304,16 +430,30 @@ export async function acquireRemoteTransport(options) {
|
|
|
304
430
|
if (!options.requiredCompanion) {
|
|
305
431
|
await (options.probeCompanion ?? assertCompanionProtocol)(companion, protocolProbeEnvironment(process.env));
|
|
306
432
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
433
|
+
const handoff = options.replaceExistingTransport && requiredExecutablePath
|
|
434
|
+
? await prepareTransportHandoff(options, fetchImpl, requiredExecutablePath)
|
|
435
|
+
: undefined;
|
|
436
|
+
if (handoff && !TRANSPORT_SECRET_PATTERN.test(handoff.inspected.rendezvous.secret)) {
|
|
437
|
+
throw new Error('Running vesper-net transport has a malformed handoff secret');
|
|
438
|
+
}
|
|
439
|
+
const baseArgs = [
|
|
309
440
|
'--mode', 'transport', '--state-dir', options.paths.tsnetDir, '--hostname', 'vesper-link',
|
|
310
441
|
'--host-url', options.hostUrl, '--rendezvous', options.paths.transportRendezvousPath,
|
|
311
442
|
'--lock', options.paths.transportLockPath, '--idle-timeout', '30s',
|
|
312
443
|
];
|
|
313
|
-
const
|
|
444
|
+
const candidateArgs = handoff
|
|
445
|
+
? [...baseArgs, '--transport-listen', new URL(handoff.inspected.rendezvous.endpoint).host]
|
|
446
|
+
: baseArgs;
|
|
447
|
+
const candidateEnvironment = companionEnvironment(process.env, bootstrapAuthKey);
|
|
448
|
+
if (handoff) {
|
|
449
|
+
candidateEnvironment[TRANSPORT_SECRET_ENVIRONMENT_KEY] = handoff.inspected.rendezvous.secret;
|
|
450
|
+
}
|
|
451
|
+
const child = (options.spawnChild ?? defaultSpawn)(companion.executable, candidateArgs, candidateEnvironment);
|
|
314
452
|
child.stdin.end();
|
|
315
453
|
let controlBuffer = '';
|
|
316
454
|
let fatalMessage;
|
|
455
|
+
let waitingForOwner = false;
|
|
456
|
+
let ownerAcquired = false;
|
|
317
457
|
child.once('error', (error) => { fatalMessage = `spawn failed: ${error.message}`; });
|
|
318
458
|
child.once('exit', (code, signal) => {
|
|
319
459
|
fatalMessage ??= `exited before ready (code=${code ?? 'none'}, signal=${signal ?? 'none'})`;
|
|
@@ -339,6 +479,12 @@ export async function acquireRemoteTransport(options) {
|
|
|
339
479
|
fatalMessage = 'vesper-net protocol mismatch';
|
|
340
480
|
child.kill('SIGTERM');
|
|
341
481
|
}
|
|
482
|
+
else if (event.type === 'waiting_for_owner') {
|
|
483
|
+
waitingForOwner = true;
|
|
484
|
+
}
|
|
485
|
+
else if (event.type === 'owner_acquired') {
|
|
486
|
+
ownerAcquired = true;
|
|
487
|
+
}
|
|
342
488
|
else if (event.type === 'needs_login' && typeof event.loginUrl === 'string') {
|
|
343
489
|
if (bootstrapAuthKey !== undefined) {
|
|
344
490
|
fatalMessage = 'automatic enrollment key was not accepted; copy a fresh Memory Link command';
|
|
@@ -373,14 +519,43 @@ export async function acquireRemoteTransport(options) {
|
|
|
373
519
|
});
|
|
374
520
|
// Never persist helper stderr. Drain it to prevent a blocked pipe.
|
|
375
521
|
child.stderr.resume();
|
|
522
|
+
const deadline = Date.now() + (options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
523
|
+
let handoffNeedsRecovery = false;
|
|
524
|
+
if (handoff) {
|
|
525
|
+
// Wait until the candidate confirms it has contended on the owner lock.
|
|
526
|
+
// This closes the stop/start gap where an old runtime could otherwise win.
|
|
527
|
+
while (!waitingForOwner && !ownerAcquired && !fatalMessage && Date.now() < deadline) {
|
|
528
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
529
|
+
}
|
|
530
|
+
if (fatalMessage) {
|
|
531
|
+
child.kill('SIGTERM');
|
|
532
|
+
return restartPriorTransport(options, undefined, baseArgs, new Error(`vesper-net transport failed: ${fatalMessage}`));
|
|
533
|
+
}
|
|
534
|
+
if (!waitingForOwner && !ownerAcquired) {
|
|
535
|
+
child.kill('SIGTERM');
|
|
536
|
+
return restartPriorTransport(options, undefined, baseArgs, new Error('Timed out waiting for vesper-net candidate to queue for transport ownership'));
|
|
537
|
+
}
|
|
538
|
+
if (waitingForOwner) {
|
|
539
|
+
try {
|
|
540
|
+
await stopTransportForHandoff(options, fetchImpl, handoff);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
child.kill('SIGTERM');
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
handoffNeedsRecovery = true;
|
|
548
|
+
}
|
|
376
549
|
while (Date.now() < deadline) {
|
|
377
550
|
let attached;
|
|
378
551
|
try {
|
|
552
|
+
if (handoff && requiredExecutablePath) {
|
|
553
|
+
await evictUnexpectedTransportOwner(options, fetchImpl, requiredExecutablePath);
|
|
554
|
+
}
|
|
379
555
|
attached = await attach(options.paths, options.hostUrl, fetchImpl, requiredExecutablePath);
|
|
380
556
|
}
|
|
381
557
|
catch (error) {
|
|
382
|
-
child
|
|
383
|
-
throw error;
|
|
558
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
384
559
|
}
|
|
385
560
|
if (attached) {
|
|
386
561
|
if (attached.ownerPid === child.pid) {
|
|
@@ -394,11 +569,11 @@ export async function acquireRemoteTransport(options) {
|
|
|
394
569
|
return attached.transport;
|
|
395
570
|
}
|
|
396
571
|
if (fatalMessage) {
|
|
397
|
-
|
|
398
|
-
|
|
572
|
+
const error = new Error(`vesper-net transport failed: ${fatalMessage}`);
|
|
573
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
399
574
|
}
|
|
400
575
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
401
576
|
}
|
|
402
|
-
|
|
403
|
-
|
|
577
|
+
const error = new Error('Timed out waiting for vesper-net transport');
|
|
578
|
+
return failAfterHandoff(options, handoffNeedsRecovery ? handoff : undefined, baseArgs, child, error);
|
|
404
579
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VESPER_LINK_VERSION = "0.1.
|
|
1
|
+
export declare const VESPER_LINK_VERSION = "0.1.6";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VESPER_LINK_VERSION = '0.1.
|
|
1
|
+
export const VESPER_LINK_VERSION = '0.1.6';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msvesper/vesper-link",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Connect agent sessions and tools to Vesper Memory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"optionalDependencies": {
|
|
31
|
-
"@msvesper/vesper-net-linux-x64": "0.1.
|
|
32
|
-
"@msvesper/vesper-net-win32-x64": "0.1.
|
|
31
|
+
"@msvesper/vesper-net-linux-x64": "0.1.6",
|
|
32
|
+
"@msvesper/vesper-net-win32-x64": "0.1.6"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.15.0",
|