@juspay/neurolink 10.8.19 → 10.8.20
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/CHANGELOG.md +6 -0
- package/dist/cli/commands/proxy.js +43 -6
- package/dist/lib/proxy/globalInstaller.d.ts +6 -0
- package/dist/lib/proxy/globalInstaller.js +30 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +48 -6
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxy.d.ts +8 -2
- package/dist/proxy/globalInstaller.d.ts +6 -0
- package/dist/proxy/globalInstaller.js +30 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +48 -6
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxy.d.ts +8 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [10.8.20](https://github.com/juspay/neurolink/compare/v10.8.19...v10.8.20) (2026-08-05)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **(proxy):** retry transient updater installs and expose worker exits ([756d04e](https://github.com/juspay/neurolink/commit/756d04ed2b39005c9b49917abdad6df78a4e251b))
|
|
6
|
+
|
|
1
7
|
## [10.8.19](https://github.com/juspay/neurolink/compare/v10.8.18...v10.8.19) (2026-08-05)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -25,7 +25,7 @@ import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
|
|
|
25
25
|
import { anthropicAccountKeysEqual, createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../lib/proxy/accountSelection.js";
|
|
26
26
|
import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../lib/proxy/proxyActivity.js";
|
|
27
27
|
import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../lib/proxy/proxyLifecycle.js";
|
|
28
|
-
import { describeInstallFailure, getGlobalInstallArgs, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
|
|
28
|
+
import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
|
|
29
29
|
import { startUpdaterWorkerSupervisor } from "../../lib/proxy/updaterSupervisor.js";
|
|
30
30
|
import { openProxyWorkerLog } from "../../lib/proxy/workerLog.js";
|
|
31
31
|
import { startRollingProxyServer } from "../../lib/proxy/rollingProxyServer.js";
|
|
@@ -671,7 +671,22 @@ async function getRollingActivationFailure(host, port, expectedVersion) {
|
|
|
671
671
|
typeof failure.message !== "string") {
|
|
672
672
|
return null;
|
|
673
673
|
}
|
|
674
|
-
|
|
674
|
+
const workerDetail = [
|
|
675
|
+
typeof failure.workerPid === "number" ? `pid=${failure.workerPid}` : null,
|
|
676
|
+
typeof failure.workerExitCode === "number" ||
|
|
677
|
+
failure.workerExitCode === null
|
|
678
|
+
? `exitCode=${failure.workerExitCode ?? "none"}`
|
|
679
|
+
: null,
|
|
680
|
+
typeof failure.workerExitSignal === "string"
|
|
681
|
+
? `exitSignal=${failure.workerExitSignal}`
|
|
682
|
+
: null,
|
|
683
|
+
typeof failure.supervisorAction === "string"
|
|
684
|
+
? `supervisorAction=${failure.supervisorAction}`
|
|
685
|
+
: null,
|
|
686
|
+
]
|
|
687
|
+
.filter(Boolean)
|
|
688
|
+
.join(" ");
|
|
689
|
+
return `${failure.phase}: ${failure.message}${workerDetail ? ` (${workerDetail})` : ""}`;
|
|
675
690
|
}
|
|
676
691
|
catch {
|
|
677
692
|
return null;
|
|
@@ -3252,6 +3267,8 @@ export const proxyGuardCommand = {
|
|
|
3252
3267
|
const NATURAL_WINDOW_WAIT_MS = 10 * 60 * 1000; // prefer no admission pause
|
|
3253
3268
|
const UPDATE_DRAIN_TIMEOUT_MS = 30 * 60 * 1000; // preserve long streams
|
|
3254
3269
|
const UPDATE_RETRY_DELAY_MS = 5 * 60 * 1000;
|
|
3270
|
+
const UPDATE_RETRY_MAX_DELAY_MS = 30 * 60 * 1000;
|
|
3271
|
+
const UPDATE_RETRY_MAX_ATTEMPTS = 4;
|
|
3255
3272
|
const UPDATE_ACTIVITY_POLL_MS = 10 * 1000;
|
|
3256
3273
|
const UPDATE_TIMEOUT_MS = 30 * 1000; // 30 seconds to come healthy
|
|
3257
3274
|
// Get running version from /health endpoint (with timeout to avoid hanging)
|
|
@@ -3276,6 +3293,8 @@ export const proxyGuardCommand = {
|
|
|
3276
3293
|
let updateCheckTimeout;
|
|
3277
3294
|
let updateCheckInterval;
|
|
3278
3295
|
let updateRetryTimeout;
|
|
3296
|
+
let updateRetryAttempts = 0;
|
|
3297
|
+
let updateRetryVersion = null;
|
|
3279
3298
|
const stopUpdateChecks = () => {
|
|
3280
3299
|
guardStopping = true;
|
|
3281
3300
|
if (updateCheckTimeout) {
|
|
@@ -3317,6 +3336,8 @@ export const proxyGuardCommand = {
|
|
|
3317
3336
|
updateVersion = result.latestVersion;
|
|
3318
3337
|
persistUpdaterState("record update check", () => recordCheck(result.latestVersion));
|
|
3319
3338
|
if (!result.updateAvailable) {
|
|
3339
|
+
updateRetryAttempts = 0;
|
|
3340
|
+
updateRetryVersion = null;
|
|
3320
3341
|
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
3321
3342
|
return;
|
|
3322
3343
|
}
|
|
@@ -3368,7 +3389,7 @@ export const proxyGuardCommand = {
|
|
|
3368
3389
|
if (updateWindow.reason === "drain_failed") {
|
|
3369
3390
|
persistUpdaterState("record unavailable update drain", () => recordUpdateDeferred(result.latestVersion, "drain_unavailable", null));
|
|
3370
3391
|
}
|
|
3371
|
-
scheduleUpdateRetry();
|
|
3392
|
+
scheduleUpdateRetry("update window unavailable", result.latestVersion);
|
|
3372
3393
|
return;
|
|
3373
3394
|
}
|
|
3374
3395
|
// Hold admission closed through install and restart after a quiet
|
|
@@ -3376,7 +3397,7 @@ export const proxyGuardCommand = {
|
|
|
3376
3397
|
if (!drainActive) {
|
|
3377
3398
|
updateWindow = await waitForWindow(0);
|
|
3378
3399
|
if (!updateWindow.ready) {
|
|
3379
|
-
scheduleUpdateRetry();
|
|
3400
|
+
scheduleUpdateRetry("update drain unavailable", result.latestVersion);
|
|
3380
3401
|
return;
|
|
3381
3402
|
}
|
|
3382
3403
|
}
|
|
@@ -3437,6 +3458,9 @@ export const proxyGuardCommand = {
|
|
|
3437
3458
|
const detail = describeInstallFailure(installErr);
|
|
3438
3459
|
logger.always(`[updater] WARNING: global install failed:\n${detail}`);
|
|
3439
3460
|
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "install", detail));
|
|
3461
|
+
if (isTransientInstallFailure(installErr)) {
|
|
3462
|
+
scheduleUpdateRetry("transient install failure", result.latestVersion);
|
|
3463
|
+
}
|
|
3440
3464
|
return;
|
|
3441
3465
|
}
|
|
3442
3466
|
}
|
|
@@ -3459,6 +3483,8 @@ export const proxyGuardCommand = {
|
|
|
3459
3483
|
return;
|
|
3460
3484
|
}
|
|
3461
3485
|
persistUpdaterState("record installed update", () => recordUpdateInstalled(result.latestVersion));
|
|
3486
|
+
updateRetryAttempts = 0;
|
|
3487
|
+
updateRetryVersion = null;
|
|
3462
3488
|
logger.always(`[updater] trampoline validated at v${validation.version} after ${validation.attempts} attempt(s)`);
|
|
3463
3489
|
}
|
|
3464
3490
|
catch (trampolineError) {
|
|
@@ -3610,14 +3636,25 @@ export const proxyGuardCommand = {
|
|
|
3610
3636
|
updateInProgress = false;
|
|
3611
3637
|
}
|
|
3612
3638
|
};
|
|
3613
|
-
const scheduleUpdateRetry = () => {
|
|
3639
|
+
const scheduleUpdateRetry = (reason, version) => {
|
|
3614
3640
|
if (guardStopping || updateRetryTimeout) {
|
|
3615
3641
|
return;
|
|
3616
3642
|
}
|
|
3643
|
+
if (updateRetryVersion !== version) {
|
|
3644
|
+
updateRetryVersion = version;
|
|
3645
|
+
updateRetryAttempts = 0;
|
|
3646
|
+
}
|
|
3647
|
+
if (updateRetryAttempts >= UPDATE_RETRY_MAX_ATTEMPTS) {
|
|
3648
|
+
logger.always(`[updater] retry budget exhausted after ${updateRetryAttempts} attempt(s); waiting for the periodic check`);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
updateRetryAttempts += 1;
|
|
3652
|
+
const delay = Math.min(UPDATE_RETRY_MAX_DELAY_MS, UPDATE_RETRY_DELAY_MS * 2 ** (updateRetryAttempts - 1));
|
|
3653
|
+
logger.always(`[updater] retry scheduled reason=${reason} attempt=${updateRetryAttempts}/${UPDATE_RETRY_MAX_ATTEMPTS} delayMs=${delay}`);
|
|
3617
3654
|
updateRetryTimeout = setTimeout(() => {
|
|
3618
3655
|
updateRetryTimeout = undefined;
|
|
3619
3656
|
void runUpdateCheck();
|
|
3620
|
-
},
|
|
3657
|
+
}, delay);
|
|
3621
3658
|
updateRetryTimeout.unref?.();
|
|
3622
3659
|
};
|
|
3623
3660
|
// Run first check after a short delay, then on interval
|
|
@@ -2,6 +2,12 @@ import type { GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionVa
|
|
|
2
2
|
/** Resolve a package manager that can update the installation currently running. */
|
|
3
3
|
export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
|
|
4
4
|
export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Return whether a global package-manager failure is likely environmental and
|
|
7
|
+
* worth retrying. Configuration and permission failures deliberately return
|
|
8
|
+
* false so the updater does not repeatedly mutate a broken installation.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isTransientInstallFailure(error: unknown): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* Validate a freshly installed CLI with retries. Global package replacement
|
|
7
13
|
* can leave executable shims briefly unavailable while filesystem metadata and
|
|
@@ -136,6 +136,36 @@ export function getGlobalInstallArgs(kind, packageSpec) {
|
|
|
136
136
|
? ["add", "-g", packageSpec]
|
|
137
137
|
: ["install", "--global", "--no-audit", "--no-fund", packageSpec];
|
|
138
138
|
}
|
|
139
|
+
const TRANSIENT_INSTALL_FAILURE_CODES = new Set([
|
|
140
|
+
"ECONNRESET",
|
|
141
|
+
"ECONNREFUSED",
|
|
142
|
+
"EAI_AGAIN",
|
|
143
|
+
"EHOSTUNREACH",
|
|
144
|
+
"ENETUNREACH",
|
|
145
|
+
"ENOTFOUND",
|
|
146
|
+
"EPIPE",
|
|
147
|
+
"ETIMEDOUT",
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* Return whether a global package-manager failure is likely environmental and
|
|
151
|
+
* worth retrying. Configuration and permission failures deliberately return
|
|
152
|
+
* false so the updater does not repeatedly mutate a broken installation.
|
|
153
|
+
*/
|
|
154
|
+
export function isTransientInstallFailure(error) {
|
|
155
|
+
let current = error;
|
|
156
|
+
for (let depth = 0; depth < 5 && current; depth++) {
|
|
157
|
+
if (typeof current !== "object") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const candidate = current;
|
|
161
|
+
if (typeof candidate.code === "string" &&
|
|
162
|
+
TRANSIENT_INSTALL_FAILURE_CODES.has(candidate.code)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
current = candidate.cause;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
139
169
|
/**
|
|
140
170
|
* Validate a freshly installed CLI with retries. Global package replacement
|
|
141
171
|
* can leave executable shims briefly unavailable while filesystem metadata and
|
|
@@ -200,7 +200,7 @@ export class RollingWorkerSupervisor {
|
|
|
200
200
|
}
|
|
201
201
|
return new Promise((resolve, reject) => {
|
|
202
202
|
let settled = false;
|
|
203
|
-
const finish = (error, phase = "startup") => {
|
|
203
|
+
const finish = (error, phase = "startup", details) => {
|
|
204
204
|
if (settled) {
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
@@ -208,7 +208,7 @@ export class RollingWorkerSupervisor {
|
|
|
208
208
|
clearTimeout(readyTimeout);
|
|
209
209
|
if (error) {
|
|
210
210
|
if (!this.closed) {
|
|
211
|
-
this.recordFailure(generation, expectedVersion, phase, error.message);
|
|
211
|
+
this.recordFailure(generation, expectedVersion, phase, error.message, details);
|
|
212
212
|
}
|
|
213
213
|
if (this.candidate?.generation === generation) {
|
|
214
214
|
this.candidate = null;
|
|
@@ -307,14 +307,22 @@ export class RollingWorkerSupervisor {
|
|
|
307
307
|
});
|
|
308
308
|
const offExit = handle.onExit((code, signal) => {
|
|
309
309
|
if (this.candidate?.generation === generation) {
|
|
310
|
-
finish(new Error(`worker ${handle.pid} exited before readiness (code=${code ?? "none"}, signal=${signal ?? "none"})`)
|
|
310
|
+
finish(new Error(`worker ${handle.pid} exited before readiness (code=${code ?? "none"}, signal=${signal ?? "none"})`), "startup", {
|
|
311
|
+
workerPid: handle.pid,
|
|
312
|
+
workerExitCode: code,
|
|
313
|
+
workerExitSignal: signal,
|
|
314
|
+
});
|
|
311
315
|
return;
|
|
312
316
|
}
|
|
313
317
|
if (this.active?.generation === generation) {
|
|
314
318
|
this.active.dispose();
|
|
315
319
|
this.active = null;
|
|
316
320
|
if (!this.closed) {
|
|
317
|
-
this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})
|
|
321
|
+
this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})`, {
|
|
322
|
+
workerPid: handle.pid,
|
|
323
|
+
workerExitCode: code,
|
|
324
|
+
workerExitSignal: signal,
|
|
325
|
+
});
|
|
318
326
|
}
|
|
319
327
|
this.options.log?.(`[proxy-supervisor] active worker exited generation=${generation} pid=${handle.pid} code=${code ?? "none"} signal=${signal ?? "none"}`);
|
|
320
328
|
}
|
|
@@ -402,7 +410,16 @@ export class RollingWorkerSupervisor {
|
|
|
402
410
|
handleTransferFailure(worker, socket, error) {
|
|
403
411
|
this.failedTransfers += 1;
|
|
404
412
|
const detail = this.describeTransferError(error);
|
|
405
|
-
this.
|
|
413
|
+
const lifecycle = this.extractLifecycleFailureDetails(error, worker.handle.pid);
|
|
414
|
+
this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket: ${detail}`, {
|
|
415
|
+
...lifecycle.details,
|
|
416
|
+
// If the error already records an exit, the supervisor did not cause
|
|
417
|
+
// that exit. Otherwise this captures the deliberate cleanup following
|
|
418
|
+
// the failed transfer, not a claimed root cause for the failure.
|
|
419
|
+
supervisorAction: lifecycle.observedExit
|
|
420
|
+
? "none"
|
|
421
|
+
: "sigkill_after_transfer_failure",
|
|
422
|
+
});
|
|
406
423
|
this.options.log?.(`[proxy-supervisor] socket transfer failed generation=${worker.generation} pid=${worker.handle.pid}: ${detail}`);
|
|
407
424
|
if (this.active?.generation === worker.generation && !this.closed) {
|
|
408
425
|
this.active = null;
|
|
@@ -427,13 +444,38 @@ export class RollingWorkerSupervisor {
|
|
|
427
444
|
const code = error.code;
|
|
428
445
|
return code ? `${code}: ${error.message}` : error.message;
|
|
429
446
|
}
|
|
430
|
-
|
|
447
|
+
extractLifecycleFailureDetails(error, fallbackPid) {
|
|
448
|
+
const context = error &&
|
|
449
|
+
typeof error === "object" &&
|
|
450
|
+
"context" in error &&
|
|
451
|
+
error.context &&
|
|
452
|
+
typeof error.context === "object"
|
|
453
|
+
? (error.context ?? {})
|
|
454
|
+
: {};
|
|
455
|
+
const workerPid = typeof context.workerPid === "number" ? context.workerPid : fallbackPid;
|
|
456
|
+
const hasExitCode = typeof context.exitCode === "number" || context.exitCode === null;
|
|
457
|
+
const hasExitSignal = typeof context.signal === "string" || context.signal === null;
|
|
458
|
+
return {
|
|
459
|
+
details: {
|
|
460
|
+
workerPid,
|
|
461
|
+
...(hasExitCode
|
|
462
|
+
? { workerExitCode: context.exitCode }
|
|
463
|
+
: {}),
|
|
464
|
+
...(hasExitSignal
|
|
465
|
+
? { workerExitSignal: context.signal }
|
|
466
|
+
: {}),
|
|
467
|
+
},
|
|
468
|
+
observedExit: hasExitCode || hasExitSignal,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
recordFailure(generation, version, phase, message, details = {}) {
|
|
431
472
|
this.lastFailure = {
|
|
432
473
|
at: new Date().toISOString(),
|
|
433
474
|
generation,
|
|
434
475
|
version,
|
|
435
476
|
phase,
|
|
436
477
|
message: message.slice(0, 1_000),
|
|
478
|
+
...details,
|
|
437
479
|
};
|
|
438
480
|
}
|
|
439
481
|
publishState() {
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -863,6 +863,10 @@ export type ProxyRollingState = {
|
|
|
863
863
|
version: string;
|
|
864
864
|
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
865
865
|
message: string;
|
|
866
|
+
workerPid?: number;
|
|
867
|
+
workerExitCode?: number | null;
|
|
868
|
+
workerExitSignal?: string | null;
|
|
869
|
+
supervisorAction?: "none" | "sigkill_after_transfer_failure";
|
|
866
870
|
} | null;
|
|
867
871
|
};
|
|
868
872
|
export type ProxySupervisorState = {
|
|
@@ -1906,6 +1906,12 @@ export type SpawnProxySocketWorkerOptions = {
|
|
|
1906
1906
|
stderr?: "inherit" | "ignore";
|
|
1907
1907
|
socketAckTimeoutMs?: number;
|
|
1908
1908
|
};
|
|
1909
|
+
export type RollingWorkerFailureDetails = {
|
|
1910
|
+
workerPid?: number;
|
|
1911
|
+
workerExitCode?: number | null;
|
|
1912
|
+
workerExitSignal?: string | null;
|
|
1913
|
+
supervisorAction?: "none" | "sigkill_after_transfer_failure";
|
|
1914
|
+
};
|
|
1909
1915
|
export type RollingWorkerSupervisorSnapshot = {
|
|
1910
1916
|
generation: number;
|
|
1911
1917
|
active: {
|
|
@@ -1926,13 +1932,13 @@ export type RollingWorkerSupervisorSnapshot = {
|
|
|
1926
1932
|
queuedSockets: number;
|
|
1927
1933
|
rejectedSockets: number;
|
|
1928
1934
|
failedTransfers: number;
|
|
1929
|
-
lastFailure: {
|
|
1935
|
+
lastFailure: ({
|
|
1930
1936
|
at: string;
|
|
1931
1937
|
generation: number;
|
|
1932
1938
|
version: string;
|
|
1933
1939
|
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
1934
1940
|
message: string;
|
|
1935
|
-
} | null;
|
|
1941
|
+
} & RollingWorkerFailureDetails) | null;
|
|
1936
1942
|
};
|
|
1937
1943
|
export type RollingWorkerSupervisorOptions = {
|
|
1938
1944
|
spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
|
|
@@ -2,6 +2,12 @@ import type { GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionVa
|
|
|
2
2
|
/** Resolve a package manager that can update the installation currently running. */
|
|
3
3
|
export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
|
|
4
4
|
export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Return whether a global package-manager failure is likely environmental and
|
|
7
|
+
* worth retrying. Configuration and permission failures deliberately return
|
|
8
|
+
* false so the updater does not repeatedly mutate a broken installation.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isTransientInstallFailure(error: unknown): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* Validate a freshly installed CLI with retries. Global package replacement
|
|
7
13
|
* can leave executable shims briefly unavailable while filesystem metadata and
|
|
@@ -136,6 +136,36 @@ export function getGlobalInstallArgs(kind, packageSpec) {
|
|
|
136
136
|
? ["add", "-g", packageSpec]
|
|
137
137
|
: ["install", "--global", "--no-audit", "--no-fund", packageSpec];
|
|
138
138
|
}
|
|
139
|
+
const TRANSIENT_INSTALL_FAILURE_CODES = new Set([
|
|
140
|
+
"ECONNRESET",
|
|
141
|
+
"ECONNREFUSED",
|
|
142
|
+
"EAI_AGAIN",
|
|
143
|
+
"EHOSTUNREACH",
|
|
144
|
+
"ENETUNREACH",
|
|
145
|
+
"ENOTFOUND",
|
|
146
|
+
"EPIPE",
|
|
147
|
+
"ETIMEDOUT",
|
|
148
|
+
]);
|
|
149
|
+
/**
|
|
150
|
+
* Return whether a global package-manager failure is likely environmental and
|
|
151
|
+
* worth retrying. Configuration and permission failures deliberately return
|
|
152
|
+
* false so the updater does not repeatedly mutate a broken installation.
|
|
153
|
+
*/
|
|
154
|
+
export function isTransientInstallFailure(error) {
|
|
155
|
+
let current = error;
|
|
156
|
+
for (let depth = 0; depth < 5 && current; depth++) {
|
|
157
|
+
if (typeof current !== "object") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const candidate = current;
|
|
161
|
+
if (typeof candidate.code === "string" &&
|
|
162
|
+
TRANSIENT_INSTALL_FAILURE_CODES.has(candidate.code)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
current = candidate.cause;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
139
169
|
/**
|
|
140
170
|
* Validate a freshly installed CLI with retries. Global package replacement
|
|
141
171
|
* can leave executable shims briefly unavailable while filesystem metadata and
|
|
@@ -200,7 +200,7 @@ export class RollingWorkerSupervisor {
|
|
|
200
200
|
}
|
|
201
201
|
return new Promise((resolve, reject) => {
|
|
202
202
|
let settled = false;
|
|
203
|
-
const finish = (error, phase = "startup") => {
|
|
203
|
+
const finish = (error, phase = "startup", details) => {
|
|
204
204
|
if (settled) {
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
@@ -208,7 +208,7 @@ export class RollingWorkerSupervisor {
|
|
|
208
208
|
clearTimeout(readyTimeout);
|
|
209
209
|
if (error) {
|
|
210
210
|
if (!this.closed) {
|
|
211
|
-
this.recordFailure(generation, expectedVersion, phase, error.message);
|
|
211
|
+
this.recordFailure(generation, expectedVersion, phase, error.message, details);
|
|
212
212
|
}
|
|
213
213
|
if (this.candidate?.generation === generation) {
|
|
214
214
|
this.candidate = null;
|
|
@@ -307,14 +307,22 @@ export class RollingWorkerSupervisor {
|
|
|
307
307
|
});
|
|
308
308
|
const offExit = handle.onExit((code, signal) => {
|
|
309
309
|
if (this.candidate?.generation === generation) {
|
|
310
|
-
finish(new Error(`worker ${handle.pid} exited before readiness (code=${code ?? "none"}, signal=${signal ?? "none"})`)
|
|
310
|
+
finish(new Error(`worker ${handle.pid} exited before readiness (code=${code ?? "none"}, signal=${signal ?? "none"})`), "startup", {
|
|
311
|
+
workerPid: handle.pid,
|
|
312
|
+
workerExitCode: code,
|
|
313
|
+
workerExitSignal: signal,
|
|
314
|
+
});
|
|
311
315
|
return;
|
|
312
316
|
}
|
|
313
317
|
if (this.active?.generation === generation) {
|
|
314
318
|
this.active.dispose();
|
|
315
319
|
this.active = null;
|
|
316
320
|
if (!this.closed) {
|
|
317
|
-
this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})
|
|
321
|
+
this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})`, {
|
|
322
|
+
workerPid: handle.pid,
|
|
323
|
+
workerExitCode: code,
|
|
324
|
+
workerExitSignal: signal,
|
|
325
|
+
});
|
|
318
326
|
}
|
|
319
327
|
this.options.log?.(`[proxy-supervisor] active worker exited generation=${generation} pid=${handle.pid} code=${code ?? "none"} signal=${signal ?? "none"}`);
|
|
320
328
|
}
|
|
@@ -402,7 +410,16 @@ export class RollingWorkerSupervisor {
|
|
|
402
410
|
handleTransferFailure(worker, socket, error) {
|
|
403
411
|
this.failedTransfers += 1;
|
|
404
412
|
const detail = this.describeTransferError(error);
|
|
405
|
-
this.
|
|
413
|
+
const lifecycle = this.extractLifecycleFailureDetails(error, worker.handle.pid);
|
|
414
|
+
this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket: ${detail}`, {
|
|
415
|
+
...lifecycle.details,
|
|
416
|
+
// If the error already records an exit, the supervisor did not cause
|
|
417
|
+
// that exit. Otherwise this captures the deliberate cleanup following
|
|
418
|
+
// the failed transfer, not a claimed root cause for the failure.
|
|
419
|
+
supervisorAction: lifecycle.observedExit
|
|
420
|
+
? "none"
|
|
421
|
+
: "sigkill_after_transfer_failure",
|
|
422
|
+
});
|
|
406
423
|
this.options.log?.(`[proxy-supervisor] socket transfer failed generation=${worker.generation} pid=${worker.handle.pid}: ${detail}`);
|
|
407
424
|
if (this.active?.generation === worker.generation && !this.closed) {
|
|
408
425
|
this.active = null;
|
|
@@ -427,13 +444,38 @@ export class RollingWorkerSupervisor {
|
|
|
427
444
|
const code = error.code;
|
|
428
445
|
return code ? `${code}: ${error.message}` : error.message;
|
|
429
446
|
}
|
|
430
|
-
|
|
447
|
+
extractLifecycleFailureDetails(error, fallbackPid) {
|
|
448
|
+
const context = error &&
|
|
449
|
+
typeof error === "object" &&
|
|
450
|
+
"context" in error &&
|
|
451
|
+
error.context &&
|
|
452
|
+
typeof error.context === "object"
|
|
453
|
+
? (error.context ?? {})
|
|
454
|
+
: {};
|
|
455
|
+
const workerPid = typeof context.workerPid === "number" ? context.workerPid : fallbackPid;
|
|
456
|
+
const hasExitCode = typeof context.exitCode === "number" || context.exitCode === null;
|
|
457
|
+
const hasExitSignal = typeof context.signal === "string" || context.signal === null;
|
|
458
|
+
return {
|
|
459
|
+
details: {
|
|
460
|
+
workerPid,
|
|
461
|
+
...(hasExitCode
|
|
462
|
+
? { workerExitCode: context.exitCode }
|
|
463
|
+
: {}),
|
|
464
|
+
...(hasExitSignal
|
|
465
|
+
? { workerExitSignal: context.signal }
|
|
466
|
+
: {}),
|
|
467
|
+
},
|
|
468
|
+
observedExit: hasExitCode || hasExitSignal,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
recordFailure(generation, version, phase, message, details = {}) {
|
|
431
472
|
this.lastFailure = {
|
|
432
473
|
at: new Date().toISOString(),
|
|
433
474
|
generation,
|
|
434
475
|
version,
|
|
435
476
|
phase,
|
|
436
477
|
message: message.slice(0, 1_000),
|
|
478
|
+
...details,
|
|
437
479
|
};
|
|
438
480
|
}
|
|
439
481
|
publishState() {
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -863,6 +863,10 @@ export type ProxyRollingState = {
|
|
|
863
863
|
version: string;
|
|
864
864
|
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
865
865
|
message: string;
|
|
866
|
+
workerPid?: number;
|
|
867
|
+
workerExitCode?: number | null;
|
|
868
|
+
workerExitSignal?: string | null;
|
|
869
|
+
supervisorAction?: "none" | "sigkill_after_transfer_failure";
|
|
866
870
|
} | null;
|
|
867
871
|
};
|
|
868
872
|
export type ProxySupervisorState = {
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -1906,6 +1906,12 @@ export type SpawnProxySocketWorkerOptions = {
|
|
|
1906
1906
|
stderr?: "inherit" | "ignore";
|
|
1907
1907
|
socketAckTimeoutMs?: number;
|
|
1908
1908
|
};
|
|
1909
|
+
export type RollingWorkerFailureDetails = {
|
|
1910
|
+
workerPid?: number;
|
|
1911
|
+
workerExitCode?: number | null;
|
|
1912
|
+
workerExitSignal?: string | null;
|
|
1913
|
+
supervisorAction?: "none" | "sigkill_after_transfer_failure";
|
|
1914
|
+
};
|
|
1909
1915
|
export type RollingWorkerSupervisorSnapshot = {
|
|
1910
1916
|
generation: number;
|
|
1911
1917
|
active: {
|
|
@@ -1926,13 +1932,13 @@ export type RollingWorkerSupervisorSnapshot = {
|
|
|
1926
1932
|
queuedSockets: number;
|
|
1927
1933
|
rejectedSockets: number;
|
|
1928
1934
|
failedTransfers: number;
|
|
1929
|
-
lastFailure: {
|
|
1935
|
+
lastFailure: ({
|
|
1930
1936
|
at: string;
|
|
1931
1937
|
generation: number;
|
|
1932
1938
|
version: string;
|
|
1933
1939
|
phase: "startup" | "activation" | "runtime" | "transfer";
|
|
1934
1940
|
message: string;
|
|
1935
|
-
} | null;
|
|
1941
|
+
} & RollingWorkerFailureDetails) | null;
|
|
1936
1942
|
};
|
|
1937
1943
|
export type RollingWorkerSupervisorOptions = {
|
|
1938
1944
|
spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "10.8.
|
|
3
|
+
"version": "10.8.20",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"test:model-pool": "npx tsx test/continuous-test-suite-model-pool.ts",
|
|
144
144
|
"test:model-not-found-retryable": "npx tsx test/continuous-test-suite-model-not-found-retryable.ts",
|
|
145
145
|
"test:model-capabilities": "npx tsx test/continuous-test-suite-model-capabilities.ts",
|
|
146
|
-
"test:agent-runtime:vitest": "pnpm exec vitest run test/agentRuntime.test.ts test/
|
|
146
|
+
"test:agent-runtime:vitest": "pnpm exec vitest run test/agentRuntime.test.ts test/agentPlumbing.test.ts test/toolExecutionRecorder.test.ts",
|
|
147
147
|
"test:retry-after:vitest": "pnpm exec vitest run test/retryAfter.test.ts",
|
|
148
148
|
"test:ci": "pnpm run test && pnpm run test:client && pnpm run test:hitl",
|
|
149
149
|
"// CI tier — fast, no live AI calls, safe for every commit": "",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"test:system-messages": "npx tsx test/continuous-test-suite-system-messages.ts",
|
|
153
153
|
"test:test-stubs": "npx tsx test/continuous-test-suite-test-stubs.ts",
|
|
154
154
|
"test:tool-routing-semantic": "npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
155
|
-
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:litellm-context && pnpm run test:dedup-execute-map && pnpm run test:step-budget-guard:vitest && pnpm run test:system-messages && pnpm run test:tool-routing-semantic && pnpm run test:anthropic-tools-policy && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities && pnpm run test:agent-runtime:vitest && pnpm run test:retry-after:vitest && pnpm run test:sampling-params && pnpm run test:structured-recovery && pnpm run test:prompt-redaction && pnpm run test:mcp-result-cache && pnpm run test:test-stubs && pnpm run test:model-not-found-retryable",
|
|
155
|
+
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:file-detector-extension && pnpm run test:file-detector-magic-bytes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:litellm-context && pnpm run test:dedup-execute-map && pnpm run test:step-budget-guard:vitest && pnpm run test:system-messages && pnpm run test:tool-routing-semantic && pnpm run test:anthropic-tools-policy && pnpm run test:sagemaker-tools && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop && pnpm run test:model-capabilities && pnpm run test:agent-runtime:vitest && pnpm run test:agent-delegation && pnpm run test:retry-after:vitest && pnpm run test:sampling-params && pnpm run test:structured-recovery && pnpm run test:prompt-redaction && pnpm run test:mcp-result-cache && pnpm run test:test-stubs && pnpm run test:model-not-found-retryable",
|
|
156
156
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
|
|
157
157
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
158
158
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",
|
|
@@ -216,6 +216,7 @@
|
|
|
216
216
|
"check:all": "pnpm run lint && pnpm run format --check && pnpm run validate && pnpm run validate:commit",
|
|
217
217
|
"test:litellm-context": "npx tsx test/continuous-test-suite-litellm-context-windows.ts",
|
|
218
218
|
"test:dedup-execute-map": "npx tsx test/continuous-test-suite-dedup-execute-map.ts",
|
|
219
|
+
"test:agent-delegation": "npx tsx test/continuous-test-suite-agent-delegation.ts",
|
|
219
220
|
"test:step-budget-guard:vitest": "pnpm exec vitest run test/stepBudgetGuard.test.ts",
|
|
220
221
|
"test:audio": "npx tsx test/continuous-test-suite-audio.ts",
|
|
221
222
|
"test:office": "npx tsx test/continuous-test-suite-office.ts",
|