@juspay/neurolink 9.93.0 → 9.93.2
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 +12 -0
- package/dist/browser/neurolink.min.js +379 -382
- package/dist/cli/commands/proxy.d.ts +3 -2
- package/dist/cli/commands/proxy.js +224 -66
- package/dist/cli/commands/proxyAnalyze.d.ts +3 -0
- package/dist/cli/commands/proxyAnalyze.js +147 -0
- package/dist/cli/parser.js +3 -1
- package/dist/cli/utils/serverUtils.js +4 -1
- package/dist/lib/proxy/globalInstaller.js +1 -1
- package/dist/lib/proxy/openaiFormat.js +1 -0
- package/dist/lib/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/lib/proxy/proxyAnalysis.js +454 -0
- package/dist/lib/proxy/proxyHealth.d.ts +4 -0
- package/dist/lib/proxy/proxyHealth.js +21 -0
- package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
- package/dist/lib/proxy/sseInterceptor.js +7 -6
- package/dist/lib/proxy/streamOutcome.d.ts +3 -1
- package/dist/lib/proxy/streamOutcome.js +77 -0
- package/dist/lib/proxy/updateCoordinator.d.ts +7 -0
- package/dist/lib/proxy/updateCoordinator.js +60 -0
- package/dist/lib/proxy/updateState.d.ts +4 -0
- package/dist/lib/proxy/updateState.js +51 -0
- package/dist/lib/proxy/workerLog.d.ts +6 -0
- package/dist/lib/proxy/workerLog.js +42 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +114 -17
- package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/lib/types/cli.d.ts +7 -0
- package/dist/lib/types/proxy.d.ts +187 -0
- package/dist/proxy/globalInstaller.js +1 -1
- package/dist/proxy/openaiFormat.js +1 -0
- package/dist/proxy/proxyAnalysis.d.ts +3 -0
- package/dist/proxy/proxyAnalysis.js +453 -0
- package/dist/proxy/proxyHealth.d.ts +4 -0
- package/dist/proxy/proxyHealth.js +21 -0
- package/dist/proxy/sseInterceptor.d.ts +9 -1
- package/dist/proxy/sseInterceptor.js +7 -6
- package/dist/proxy/streamOutcome.d.ts +3 -1
- package/dist/proxy/streamOutcome.js +77 -0
- package/dist/proxy/updateCoordinator.d.ts +7 -0
- package/dist/proxy/updateCoordinator.js +59 -0
- package/dist/proxy/updateState.d.ts +4 -0
- package/dist/proxy/updateState.js +51 -0
- package/dist/proxy/workerLog.d.ts +6 -0
- package/dist/proxy/workerLog.js +41 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +114 -17
- package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
- package/dist/server/routes/openaiProxyRoutes.js +16 -1
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/proxy.d.ts +187 -0
- package/package.json +4 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { CommandModule } from "yargs";
|
|
13
13
|
import type { Hono } from "hono";
|
|
14
|
-
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs } from "../../lib/types/index.js";
|
|
14
|
+
import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
|
|
15
15
|
import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
|
|
16
16
|
export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
|
|
17
17
|
export declare function createProxyStartApp(params: {
|
|
@@ -25,9 +25,10 @@ export declare function createProxyStartApp(params: {
|
|
|
25
25
|
primaryAccountKey: string | undefined;
|
|
26
26
|
accountAllowlist: AccountAllowlist | undefined;
|
|
27
27
|
runtimeConfigStore?: ProxyRuntimeConfigStore;
|
|
28
|
+
updateControlToken?: string;
|
|
28
29
|
}): Promise<{
|
|
29
30
|
app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
30
|
-
readiness:
|
|
31
|
+
readiness: ProxyReadinessState;
|
|
31
32
|
}>;
|
|
32
33
|
export declare const proxyStartCommand: CommandModule<object, ProxyStartArgs>;
|
|
33
34
|
export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
|
|
@@ -14,8 +14,9 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import chalk from "chalk";
|
|
16
16
|
import ora from "ora";
|
|
17
|
-
import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
|
|
17
|
+
import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
|
|
18
18
|
import { logger } from "../../lib/utils/logger.js";
|
|
19
|
+
import { sanitizeForLog } from "../../lib/utils/logSanitize.js";
|
|
19
20
|
import { withTimeout } from "../../lib/utils/async/withTimeout.js";
|
|
20
21
|
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
|
|
21
22
|
import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
|
|
@@ -25,7 +26,9 @@ import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from
|
|
|
25
26
|
import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../lib/proxy/proxyLifecycle.js";
|
|
26
27
|
import { describeInstallFailure, getGlobalInstallArgs, resolveGlobalInstaller, validateInstalledVersion, } from "../../lib/proxy/globalInstaller.js";
|
|
27
28
|
import { startUpdaterWorkerSupervisor } from "../../lib/proxy/updaterSupervisor.js";
|
|
28
|
-
import {
|
|
29
|
+
import { openProxyWorkerLog } from "../../lib/proxy/workerLog.js";
|
|
30
|
+
import { waitForProxyUpdateWindow } from "../../lib/proxy/updateCoordinator.js";
|
|
31
|
+
import { abandonPendingUpdate, clearUpdateDeferral, isVersionSuppressed, loadUpdateState, recordCheck, recordSuccessfulUpdate, recordUpdateDeferred, recordUpdateFailure, recordUpdateInstalled, suppressVersion, } from "../../lib/proxy/updateState.js";
|
|
29
32
|
import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../lib/proxy/proxyEnv.js";
|
|
30
33
|
import { createRequire } from "node:module";
|
|
31
34
|
import { fileURLToPath } from "node:url";
|
|
@@ -34,6 +37,8 @@ const _require = createRequire(import.meta.url);
|
|
|
34
37
|
const PROXY_VERSION = packageJson.version;
|
|
35
38
|
const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
|
|
36
39
|
const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
40
|
+
const PROXY_UPDATE_CONTROL_TOKEN = process.env.NEUROLINK_PROXY_UPDATE_CONTROL_TOKEN?.trim() ||
|
|
41
|
+
crypto.randomUUID();
|
|
37
42
|
// =============================================================================
|
|
38
43
|
// STATE MANAGEMENT
|
|
39
44
|
// =============================================================================
|
|
@@ -370,16 +375,52 @@ async function getProxyRuntimeActivity(host, port, timeoutMs = 3_000) {
|
|
|
370
375
|
return null;
|
|
371
376
|
}
|
|
372
377
|
}
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
async function setProxyUpdateDrain(host, port, draining) {
|
|
379
|
+
const confirmState = async () => {
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch(`http://${host}:${port}/status`, {
|
|
382
|
+
signal: AbortSignal.timeout(3_000),
|
|
383
|
+
});
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const payload = (await response.json());
|
|
388
|
+
return payload.health?.drainingForUpdate === draining;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(`http://${host}:${port}/internal/update-control`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: {
|
|
398
|
+
"content-type": "application/json",
|
|
399
|
+
"x-neurolink-update-token": PROXY_UPDATE_CONTROL_TOKEN,
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({ action: draining ? "drain" : "resume" }),
|
|
402
|
+
signal: AbortSignal.timeout(5_000),
|
|
403
|
+
});
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
return confirmState();
|
|
406
|
+
}
|
|
407
|
+
const payload = (await response.json());
|
|
408
|
+
return payload.draining === draining;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return confirmState();
|
|
376
412
|
}
|
|
377
|
-
|
|
378
|
-
|
|
413
|
+
}
|
|
414
|
+
async function resumeProxyUpdateDrain(host, port, attempts = 3) {
|
|
415
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
416
|
+
if (await setProxyUpdateDrain(host, port, false)) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
if (attempt < attempts) {
|
|
420
|
+
await sleep(1_000);
|
|
421
|
+
}
|
|
379
422
|
}
|
|
380
|
-
|
|
381
|
-
return (Number.isFinite(lastActivityMs) &&
|
|
382
|
-
nowMs - lastActivityMs >= quietThresholdMs);
|
|
423
|
+
return false;
|
|
383
424
|
}
|
|
384
425
|
// ---------------------------------------------------------------------------
|
|
385
426
|
// Stable entrypoint for launchd
|
|
@@ -499,18 +540,14 @@ function spawnFailOpenGuard(host, port, parentPid) {
|
|
|
499
540
|
String(parentPid),
|
|
500
541
|
"--quiet",
|
|
501
542
|
];
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
mkdirSync(guardLogDir, { recursive: true });
|
|
507
|
-
}
|
|
508
|
-
const guardLogPath = join(guardLogDir, "proxy-guard.log");
|
|
509
|
-
const logFd = openSync(guardLogPath, "a");
|
|
543
|
+
const workerLog = openProxyWorkerLog("proxy-guard.log");
|
|
544
|
+
if (workerLog.error) {
|
|
545
|
+
logger.debug(`[proxy] guard logging disabled: ${workerLog.error}`);
|
|
546
|
+
}
|
|
510
547
|
try {
|
|
511
548
|
const child = spawn(process.execPath, args, {
|
|
512
549
|
detached: true,
|
|
513
|
-
stdio: ["ignore",
|
|
550
|
+
stdio: ["ignore", workerLog.stdio, workerLog.stdio],
|
|
514
551
|
});
|
|
515
552
|
child.unref();
|
|
516
553
|
return child.pid;
|
|
@@ -520,7 +557,7 @@ function spawnFailOpenGuard(host, port, parentPid) {
|
|
|
520
557
|
return undefined;
|
|
521
558
|
}
|
|
522
559
|
finally {
|
|
523
|
-
|
|
560
|
+
workerLog.close();
|
|
524
561
|
}
|
|
525
562
|
}
|
|
526
563
|
function spawnProxyUpdater(host, port, parentPid) {
|
|
@@ -546,17 +583,18 @@ function spawnProxyUpdater(host, port, parentPid) {
|
|
|
546
583
|
"--updater-only",
|
|
547
584
|
"--quiet",
|
|
548
585
|
];
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
mkdirSync(logsDir, { recursive: true });
|
|
586
|
+
const workerLog = openProxyWorkerLog("proxy-updater.log");
|
|
587
|
+
if (workerLog.error) {
|
|
588
|
+
logger.always(`[proxy] updater logging disabled: ${workerLog.error}`);
|
|
553
589
|
}
|
|
554
|
-
const logFd = openSync(join(logsDir, "proxy-updater.log"), "a");
|
|
555
590
|
try {
|
|
556
591
|
const child = spawn(process.execPath, args, {
|
|
557
592
|
detached: true,
|
|
558
|
-
stdio: ["ignore",
|
|
559
|
-
env:
|
|
593
|
+
stdio: ["ignore", workerLog.stdio, workerLog.stdio],
|
|
594
|
+
env: {
|
|
595
|
+
...process.env,
|
|
596
|
+
NEUROLINK_PROXY_UPDATE_CONTROL_TOKEN: PROXY_UPDATE_CONTROL_TOKEN,
|
|
597
|
+
},
|
|
560
598
|
});
|
|
561
599
|
child.once("error", (error) => {
|
|
562
600
|
logger.always(`[proxy] updater worker error pid=${child.pid ?? "unknown"}: ${error.message}`);
|
|
@@ -572,7 +610,7 @@ function spawnProxyUpdater(host, port, parentPid) {
|
|
|
572
610
|
return undefined;
|
|
573
611
|
}
|
|
574
612
|
finally {
|
|
575
|
-
|
|
613
|
+
workerLog.close();
|
|
576
614
|
}
|
|
577
615
|
}
|
|
578
616
|
async function runProxyTelemetryManager(command) {
|
|
@@ -727,7 +765,7 @@ async function createProxyNeurolinkRuntime(logsDir) {
|
|
|
727
765
|
cleanupLogs(7, 500);
|
|
728
766
|
return { neurolink, cleanupLogs };
|
|
729
767
|
}
|
|
730
|
-
function registerProxyRequestTracking(app, requestMetadata) {
|
|
768
|
+
function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
731
769
|
app.use("/v1/*", async (c, next) => {
|
|
732
770
|
const startedMonotonicMs = performance.now();
|
|
733
771
|
const contentLengthHeader = c.req.raw.headers.get("content-length");
|
|
@@ -747,9 +785,12 @@ function registerProxyRequestTracking(app, requestMetadata) {
|
|
|
747
785
|
model: "-",
|
|
748
786
|
stream: false,
|
|
749
787
|
toolCount: 0,
|
|
788
|
+
rejectForUpdate: readiness.drainingForUpdate,
|
|
750
789
|
};
|
|
751
790
|
requestMetadata.set(c.req.raw, metadata);
|
|
752
|
-
const finishActivity =
|
|
791
|
+
const finishActivity = metadata.rejectForUpdate
|
|
792
|
+
? () => undefined
|
|
793
|
+
: beginProxyRequest();
|
|
753
794
|
const finish = () => {
|
|
754
795
|
finishActivity();
|
|
755
796
|
requestMetadata.delete(c.req.raw);
|
|
@@ -924,7 +965,34 @@ export async function createProxyStartApp(params) {
|
|
|
924
965
|
},
|
|
925
966
|
}, 502);
|
|
926
967
|
});
|
|
927
|
-
|
|
968
|
+
app.post("/internal/update-control", async (c) => {
|
|
969
|
+
const suppliedToken = c.req.header("x-neurolink-update-token");
|
|
970
|
+
const expectedToken = params.updateControlToken ?? PROXY_UPDATE_CONTROL_TOKEN;
|
|
971
|
+
if (!suppliedToken || suppliedToken !== expectedToken) {
|
|
972
|
+
return c.json({ error: "not_found" }, 404);
|
|
973
|
+
}
|
|
974
|
+
const payload = await c.req
|
|
975
|
+
.json()
|
|
976
|
+
.catch(() => ({ action: undefined }));
|
|
977
|
+
if (payload.action === "drain") {
|
|
978
|
+
if (!markProxyDrainingForUpdate(readiness)) {
|
|
979
|
+
return c.json({ error: "proxy_not_ready" }, 409);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
else if (payload.action === "resume") {
|
|
983
|
+
if (!resumeProxyConnections(readiness)) {
|
|
984
|
+
return c.json({ error: "proxy_not_ready" }, 409);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
return c.json({ error: "invalid_action" }, 400);
|
|
989
|
+
}
|
|
990
|
+
return c.json({
|
|
991
|
+
draining: readiness.drainingForUpdate,
|
|
992
|
+
acceptingConnections: readiness.acceptingConnections,
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
registerProxyRequestTracking(app, requestMetadata, readiness);
|
|
928
996
|
const runtimeConfigStore = params.runtimeConfigStore;
|
|
929
997
|
const runtimeConfigProvider = runtimeConfigStore
|
|
930
998
|
? () => runtimeConfigStore.getSnapshot()
|
|
@@ -940,6 +1008,24 @@ export async function createProxyStartApp(params) {
|
|
|
940
1008
|
for (const route of allProxyRoutes) {
|
|
941
1009
|
const method = route.method.toLowerCase();
|
|
942
1010
|
app[method](route.path, async (c) => {
|
|
1011
|
+
const metadata = requestMetadata.get(c.req.raw);
|
|
1012
|
+
if (metadata?.rejectForUpdate) {
|
|
1013
|
+
if (metadata) {
|
|
1014
|
+
metadata.terminalErrorType = "proxy_draining";
|
|
1015
|
+
await recordRuntimeError(metadata, 503, "proxy_draining", "Proxy is draining for an automatic update", {
|
|
1016
|
+
clientMessage: "Proxy is restarting; retry shortly",
|
|
1017
|
+
clientErrorType: "overloaded_error",
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
c.header("Retry-After", "2");
|
|
1021
|
+
return c.json({
|
|
1022
|
+
type: "error",
|
|
1023
|
+
error: {
|
|
1024
|
+
type: "overloaded_error",
|
|
1025
|
+
message: "Proxy is restarting; retry shortly",
|
|
1026
|
+
},
|
|
1027
|
+
}, 503);
|
|
1028
|
+
}
|
|
943
1029
|
const emptyBody = {};
|
|
944
1030
|
let body;
|
|
945
1031
|
let rawBody;
|
|
@@ -970,13 +1056,13 @@ export async function createProxyStartApp(params) {
|
|
|
970
1056
|
const toolCount = Array.isArray(bodyRec?.tools)
|
|
971
1057
|
? bodyRec.tools.length
|
|
972
1058
|
: 0;
|
|
973
|
-
const metadata = requestMetadata.get(c.req.raw);
|
|
974
1059
|
if (metadata) {
|
|
975
1060
|
metadata.model = String(model);
|
|
976
1061
|
metadata.stream = stream === "stream";
|
|
977
1062
|
metadata.toolCount = toolCount;
|
|
978
1063
|
}
|
|
979
|
-
|
|
1064
|
+
const logModel = sanitizeForLog(String(model));
|
|
1065
|
+
logger.always(`[proxy] ${c.req.method} ${c.req.path} → model=${logModel} ${stream} tools=${toolCount}`);
|
|
980
1066
|
const ctx = {
|
|
981
1067
|
requestId: metadata?.requestId ?? crypto.randomUUID(),
|
|
982
1068
|
method: c.req.method,
|
|
@@ -1175,6 +1261,7 @@ export async function createProxyStartApp(params) {
|
|
|
1175
1261
|
liveVersion: PROXY_VERSION,
|
|
1176
1262
|
latestVersion: updateState?.lastCheckVersion || null,
|
|
1177
1263
|
pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
|
|
1264
|
+
deferredUpdate: updateState?.deferredUpdate ?? null,
|
|
1178
1265
|
lastCheckAt: updateState?.lastCheckAt ?? null,
|
|
1179
1266
|
lastUpdateAt: updateState?.lastUpdateAt ?? null,
|
|
1180
1267
|
lastUpdateVersion: updateState?.lastUpdateVersion ?? null,
|
|
@@ -1978,6 +2065,7 @@ export const proxyStatusCommand = {
|
|
|
1978
2065
|
updaterRunning: false,
|
|
1979
2066
|
latestVersion: updateState?.lastCheckVersion || null,
|
|
1980
2067
|
pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
|
|
2068
|
+
deferredUpdate: updateState?.deferredUpdate ?? null,
|
|
1981
2069
|
lastUpdateFailure: updateState?.lastFailure ?? null,
|
|
1982
2070
|
};
|
|
1983
2071
|
if (state && isProcessRunning(state.pid)) {
|
|
@@ -2057,6 +2145,12 @@ export const proxyStatusCommand = {
|
|
|
2057
2145
|
if (status.pendingRestartVersion) {
|
|
2058
2146
|
logger.always(` ${chalk.bold("Pending:")} ${chalk.yellow(`v${status.pendingRestartVersion} installed; restart pending`)}`);
|
|
2059
2147
|
}
|
|
2148
|
+
if (status.deferredUpdate) {
|
|
2149
|
+
const active = status.deferredUpdate.activeRequests === null
|
|
2150
|
+
? "unknown activity"
|
|
2151
|
+
: `${status.deferredUpdate.activeRequests} active request(s)`;
|
|
2152
|
+
logger.always(` ${chalk.bold("Deferred:")} ${chalk.yellow(`v${status.deferredUpdate.version} ${status.deferredUpdate.reason} (${active})`)}`);
|
|
2153
|
+
}
|
|
2060
2154
|
if (status.latestVersion) {
|
|
2061
2155
|
logger.always(` ${chalk.bold("Latest:")} ${chalk.cyan(`v${status.latestVersion}`)}`);
|
|
2062
2156
|
}
|
|
@@ -2239,6 +2333,10 @@ export const proxyGuardCommand = {
|
|
|
2239
2333
|
// foreground fail-open guard remains cleanup-only.
|
|
2240
2334
|
const UPDATE_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
2241
2335
|
const QUIET_THRESHOLD_MS = 120 * 1000; // 2 minutes of silence
|
|
2336
|
+
const NATURAL_WINDOW_WAIT_MS = 10 * 60 * 1000; // prefer no admission pause
|
|
2337
|
+
const UPDATE_DRAIN_TIMEOUT_MS = 30 * 60 * 1000; // preserve long streams
|
|
2338
|
+
const UPDATE_RETRY_DELAY_MS = 5 * 60 * 1000;
|
|
2339
|
+
const UPDATE_ACTIVITY_POLL_MS = 10 * 1000;
|
|
2242
2340
|
const UPDATE_TIMEOUT_MS = 30 * 1000; // 30 seconds to come healthy
|
|
2243
2341
|
// Get running version from /health endpoint (with timeout to avoid hanging)
|
|
2244
2342
|
let runningVersion = PROXY_VERSION; // fallback
|
|
@@ -2261,6 +2359,7 @@ export const proxyGuardCommand = {
|
|
|
2261
2359
|
let guardStopping = false;
|
|
2262
2360
|
let updateCheckTimeout;
|
|
2263
2361
|
let updateCheckInterval;
|
|
2362
|
+
let updateRetryTimeout;
|
|
2264
2363
|
const stopUpdateChecks = () => {
|
|
2265
2364
|
guardStopping = true;
|
|
2266
2365
|
if (updateCheckTimeout) {
|
|
@@ -2271,6 +2370,10 @@ export const proxyGuardCommand = {
|
|
|
2271
2370
|
clearInterval(updateCheckInterval);
|
|
2272
2371
|
updateCheckInterval = undefined;
|
|
2273
2372
|
}
|
|
2373
|
+
if (updateRetryTimeout) {
|
|
2374
|
+
clearTimeout(updateRetryTimeout);
|
|
2375
|
+
updateRetryTimeout = undefined;
|
|
2376
|
+
}
|
|
2274
2377
|
};
|
|
2275
2378
|
/** Keep state-write failures observable without terminating the updater. */
|
|
2276
2379
|
const persistUpdaterState = (operation, action) => {
|
|
@@ -2289,41 +2392,93 @@ export const proxyGuardCommand = {
|
|
|
2289
2392
|
}
|
|
2290
2393
|
updateInProgress = true;
|
|
2291
2394
|
let updateVersion = runningVersion;
|
|
2395
|
+
let drainActive = false;
|
|
2292
2396
|
try {
|
|
2293
2397
|
// Lazy-load update modules so they're only imported at check time
|
|
2294
2398
|
const { checkForUpdate } = await import("../../lib/proxy/updateChecker.js");
|
|
2295
2399
|
// 1. Check for update
|
|
2296
|
-
|
|
2400
|
+
let result = await checkForUpdate(runningVersion);
|
|
2297
2401
|
updateVersion = result.latestVersion;
|
|
2298
2402
|
persistUpdaterState("record update check", () => recordCheck(result.latestVersion));
|
|
2299
2403
|
if (!result.updateAvailable) {
|
|
2404
|
+
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
2300
2405
|
return;
|
|
2301
2406
|
}
|
|
2302
|
-
const
|
|
2303
|
-
if (isVersionSuppressed(result.latestVersion) &&
|
|
2407
|
+
const initiallyPendingRestart = loadUpdateState()?.pendingRestartVersion === result.latestVersion;
|
|
2408
|
+
if (isVersionSuppressed(result.latestVersion) &&
|
|
2409
|
+
!initiallyPendingRestart) {
|
|
2304
2410
|
logger.debug(`[guard] version ${result.latestVersion} is suppressed, skipping`);
|
|
2305
2411
|
return;
|
|
2306
2412
|
}
|
|
2307
2413
|
logger.always(`[updater] update available: ${runningVersion} → ${result.latestVersion}`);
|
|
2308
|
-
// 2.
|
|
2309
|
-
// a
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2414
|
+
// 2. Prefer a naturally quiet period. Sustained traffic is handled by
|
|
2415
|
+
// a bounded graceful drain so update checks cannot wait forever.
|
|
2416
|
+
let lastDeferralSignature = "";
|
|
2417
|
+
const waitForWindow = async (quietWaitMs) => {
|
|
2418
|
+
const window = await waitForProxyUpdateWindow({
|
|
2419
|
+
quietThresholdMs: QUIET_THRESHOLD_MS,
|
|
2420
|
+
quietWaitMs,
|
|
2421
|
+
drainWaitMs: UPDATE_DRAIN_TIMEOUT_MS,
|
|
2422
|
+
pollIntervalMs: UPDATE_ACTIVITY_POLL_MS,
|
|
2423
|
+
getActivity: () => getProxyRuntimeActivity(host, port),
|
|
2424
|
+
setDraining: async (draining) => {
|
|
2425
|
+
const changed = await setProxyUpdateDrain(host, port, draining);
|
|
2426
|
+
if (changed) {
|
|
2427
|
+
logger.always(`[updater] ${draining ? "draining new inference requests" : "inference admission resumed"}`);
|
|
2428
|
+
}
|
|
2429
|
+
return changed;
|
|
2430
|
+
},
|
|
2431
|
+
isStopping: () => guardStopping,
|
|
2432
|
+
isParentAlive: () => getProcessStatus(parentPid) !== "not_running",
|
|
2433
|
+
onPhase: (phase, activity) => {
|
|
2434
|
+
const reason = phase === "waiting_for_quiet" && !activity
|
|
2435
|
+
? "activity_unavailable"
|
|
2436
|
+
: phase;
|
|
2437
|
+
const signature = `${reason}:${activity?.activeRequests ?? "unknown"}`;
|
|
2438
|
+
if (signature !== lastDeferralSignature) {
|
|
2439
|
+
lastDeferralSignature = signature;
|
|
2440
|
+
persistUpdaterState("record update deferral", () => recordUpdateDeferred(result.latestVersion, reason, activity?.activeRequests ?? null));
|
|
2441
|
+
}
|
|
2442
|
+
logger.debug(`[updater] ${reason} (${activity?.activeRequests ?? "unknown"} active requests)`);
|
|
2443
|
+
},
|
|
2444
|
+
});
|
|
2445
|
+
drainActive = drainActive || window.draining;
|
|
2446
|
+
return window;
|
|
2447
|
+
};
|
|
2448
|
+
let updateWindow = await waitForWindow(NATURAL_WINDOW_WAIT_MS);
|
|
2449
|
+
if (!updateWindow.ready) {
|
|
2450
|
+
if (updateWindow.reason === "drain_failed") {
|
|
2451
|
+
persistUpdaterState("record unavailable update drain", () => recordUpdateDeferred(result.latestVersion, "drain_unavailable", null));
|
|
2315
2452
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2453
|
+
scheduleUpdateRetry();
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
// Close the small race between observing zero activity and package
|
|
2457
|
+
// mutation by holding admission closed through install and restart.
|
|
2458
|
+
if (!drainActive) {
|
|
2459
|
+
updateWindow = await waitForWindow(0);
|
|
2460
|
+
if (!updateWindow.ready) {
|
|
2461
|
+
scheduleUpdateRetry();
|
|
2462
|
+
return;
|
|
2320
2463
|
}
|
|
2321
|
-
logger.debug(`[updater] waiting for idle traffic (${activity?.activeRequests ?? "unknown"} active requests)`);
|
|
2322
|
-
await new Promise((r) => setTimeout(r, quietPollMs));
|
|
2323
2464
|
}
|
|
2324
|
-
|
|
2465
|
+
// Refresh after waiting so a long deferral can never install a stale
|
|
2466
|
+
// target while a newer release is already available.
|
|
2467
|
+
const refreshedResult = await checkForUpdate(runningVersion);
|
|
2468
|
+
result = refreshedResult;
|
|
2469
|
+
updateVersion = result.latestVersion;
|
|
2470
|
+
persistUpdaterState("record refreshed update check", () => recordCheck(result.latestVersion));
|
|
2471
|
+
if (!result.updateAvailable) {
|
|
2472
|
+
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
2325
2473
|
return;
|
|
2326
2474
|
}
|
|
2475
|
+
const pendingRestart = loadUpdateState()?.pendingRestartVersion === result.latestVersion;
|
|
2476
|
+
if (isVersionSuppressed(result.latestVersion) && !pendingRestart) {
|
|
2477
|
+
logger.debug(`[guard] refreshed version ${result.latestVersion} is suppressed, skipping`);
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
2481
|
+
logger.always(`[updater] safe update window acquired for v${result.latestVersion}`);
|
|
2327
2482
|
// 3. Install update (validate version string before passing to shell)
|
|
2328
2483
|
if (!/^\d+\.\d+\.\d+$/.test(result.latestVersion)) {
|
|
2329
2484
|
const message = `invalid version format: ${result.latestVersion}`;
|
|
@@ -2396,22 +2551,8 @@ export const proxyGuardCommand = {
|
|
|
2396
2551
|
if (guardStopping || getProcessStatus(parentPid) === "not_running") {
|
|
2397
2552
|
return;
|
|
2398
2553
|
}
|
|
2399
|
-
//
|
|
2400
|
-
//
|
|
2401
|
-
while (!guardStopping) {
|
|
2402
|
-
if (getProcessStatus(parentPid) === "not_running") {
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
const activity = await getProxyRuntimeActivity(host, port);
|
|
2406
|
-
if (activity && isSafeUpdateWindow(activity, QUIET_THRESHOLD_MS)) {
|
|
2407
|
-
break;
|
|
2408
|
-
}
|
|
2409
|
-
logger.debug(`[updater] update installed; deferring restart (${activity?.activeRequests ?? "unknown"} active requests)`);
|
|
2410
|
-
await sleep(10_000);
|
|
2411
|
-
}
|
|
2412
|
-
if (guardStopping) {
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2554
|
+
// Admission has remained closed since active requests reached zero, so
|
|
2555
|
+
// restart cannot interrupt an accepted request or long-lived stream.
|
|
2415
2556
|
// Signal the health loop to not exit when it detects
|
|
2416
2557
|
// the parent PID is gone — we're intentionally restarting.
|
|
2417
2558
|
updateRestartInProgress = true;
|
|
@@ -2432,6 +2573,7 @@ export const proxyGuardCommand = {
|
|
|
2432
2573
|
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "restart", msg));
|
|
2433
2574
|
return;
|
|
2434
2575
|
}
|
|
2576
|
+
drainActive = false;
|
|
2435
2577
|
// 5. Wait for healthy restart
|
|
2436
2578
|
let healthy = false;
|
|
2437
2579
|
const restartStart = Date.now();
|
|
@@ -2473,9 +2615,25 @@ export const proxyGuardCommand = {
|
|
|
2473
2615
|
persistUpdaterState("record update failure", () => recordUpdateFailure(updateVersion, "check", message));
|
|
2474
2616
|
}
|
|
2475
2617
|
finally {
|
|
2618
|
+
if (drainActive && !updateRestartInProgress) {
|
|
2619
|
+
const resumed = await resumeProxyUpdateDrain(host, port);
|
|
2620
|
+
if (!resumed && getProcessStatus(parentPid) !== "not_running") {
|
|
2621
|
+
logger.always(`[updater] WARNING: failed to resume inference admission after deferred update`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2476
2624
|
updateInProgress = false;
|
|
2477
2625
|
}
|
|
2478
2626
|
};
|
|
2627
|
+
const scheduleUpdateRetry = () => {
|
|
2628
|
+
if (guardStopping || updateRetryTimeout) {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
updateRetryTimeout = setTimeout(() => {
|
|
2632
|
+
updateRetryTimeout = undefined;
|
|
2633
|
+
void runUpdateCheck();
|
|
2634
|
+
}, UPDATE_RETRY_DELAY_MS);
|
|
2635
|
+
updateRetryTimeout.unref?.();
|
|
2636
|
+
};
|
|
2479
2637
|
// Run first check after a short delay, then on interval
|
|
2480
2638
|
if (canAutoUpdate) {
|
|
2481
2639
|
updateCheckTimeout = setTimeout(runUpdateCheck, 30_000);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { analyzeProxyLogs } from "../../lib/proxy/proxyAnalysis.js";
|
|
3
|
+
import { logger } from "../../lib/utils/logger.js";
|
|
4
|
+
function formatLatency(label, summary) {
|
|
5
|
+
const value = (amount) => amount === null ? "-" : amount.toFixed(1);
|
|
6
|
+
return `${label.padEnd(22)} ${String(summary.count).padStart(7)} ${value(summary.p50).padStart(9)} ${value(summary.p95).padStart(9)} ${value(summary.p99).padStart(9)} ${value(summary.max).padStart(9)}`;
|
|
7
|
+
}
|
|
8
|
+
function printAnalysis(report) {
|
|
9
|
+
logger.always("");
|
|
10
|
+
logger.always(chalk.bold.cyan("NeuroLink Proxy Analysis"));
|
|
11
|
+
logger.always(chalk.gray("=".repeat(50)));
|
|
12
|
+
logger.always(` Since: ${chalk.cyan(report.since)}`);
|
|
13
|
+
logger.always(` Logs: ${chalk.cyan(report.logsDir)}`);
|
|
14
|
+
logger.always(` Files: ${report.files.requests} request, ${report.files.attempts} attempt, ${report.files.lifecycle} lifecycle`);
|
|
15
|
+
logger.always("");
|
|
16
|
+
logger.always(chalk.bold(" Reliability"));
|
|
17
|
+
logger.always(report.coverage.finalRequests
|
|
18
|
+
? ` Completed: ${report.requests.completed} (${chalk.green(`${report.requests.success} success`)}, ${chalk.red(`${report.requests.errors} errors`)})`
|
|
19
|
+
: chalk.yellow(" Completed: unavailable (no final request logs)"));
|
|
20
|
+
logger.always(report.coverage.attempts && report.coverage.finalRequests
|
|
21
|
+
? ` Recovered after retry: ${report.requests.recoveredAfterRetry}`
|
|
22
|
+
: chalk.yellow(" Recovered after retry: unavailable"));
|
|
23
|
+
if (report.requests.errors > 0) {
|
|
24
|
+
logger.always(` Final error types: ${JSON.stringify(report.requests.errorTypes)}`);
|
|
25
|
+
}
|
|
26
|
+
if (report.coverage.attempts) {
|
|
27
|
+
logger.always(` Attempts: ${report.attempts.total}, ${report.attempts.errors} errors${report.attempts.errors > 0 ? ` ${JSON.stringify(report.attempts.errorTypes)}` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
logger.always(report.coverage.lifecycle
|
|
30
|
+
? ` Lifecycle: ${report.lifecycle.accepted} accepted, ${report.lifecycle.terminal} terminal, ${report.lifecycle.unsettled} unsettled`
|
|
31
|
+
: chalk.yellow(" Lifecycle: unavailable (no lifecycle metadata)"));
|
|
32
|
+
if (report.coverage.attempts) {
|
|
33
|
+
const finalRateLimits = report.coverage.finalRequests
|
|
34
|
+
? `${report.requests.finalRateLimits} final`
|
|
35
|
+
: "final unavailable";
|
|
36
|
+
logger.always(` Rate limits: ${report.rateLimits.attemptRateLimits} attempts (${report.rateLimits.quota} quota, ${report.rateLimits.transient} transient, ${report.rateLimits.unclassified} unclassified), ${finalRateLimits}`);
|
|
37
|
+
}
|
|
38
|
+
else if (report.coverage.finalRequests) {
|
|
39
|
+
logger.always(chalk.yellow(` Rate limits: attempt classification unavailable; ${report.requests.finalRateLimits} final 429 response(s)`));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
logger.always(chalk.yellow(" Rate limits: unavailable"));
|
|
43
|
+
}
|
|
44
|
+
logger.always("");
|
|
45
|
+
logger.always(chalk.bold(" Latency (ms)"));
|
|
46
|
+
const latencyHeader = `${"METRIC".padEnd(22)} ${"COUNT".padStart(7)} ${"P50".padStart(9)} ${"P95".padStart(9)} ${"P99".padStart(9)} ${"MAX".padStart(9)}`;
|
|
47
|
+
logger.always(` ${chalk.gray(latencyHeader)}`);
|
|
48
|
+
logger.always(` ${chalk.gray("-".repeat(latencyHeader.length))}`);
|
|
49
|
+
for (const [label, summary] of [
|
|
50
|
+
["Response headers", report.latencyMs.headers],
|
|
51
|
+
["First chunk", report.latencyMs.firstChunk],
|
|
52
|
+
["Terminal", report.latencyMs.terminal],
|
|
53
|
+
["Final request log", report.latencyMs.finalRequest],
|
|
54
|
+
["Account attempt", report.latencyMs.attempt],
|
|
55
|
+
["Single-attempt delta", report.latencyMs.singleAttemptDelta],
|
|
56
|
+
]) {
|
|
57
|
+
logger.always(` ${formatLatency(label, summary)}`);
|
|
58
|
+
}
|
|
59
|
+
logger.always("");
|
|
60
|
+
logger.always(chalk.bold(" Cache"));
|
|
61
|
+
if (report.coverage.cacheUsage) {
|
|
62
|
+
logger.always(` Usage records: ${report.cache.requestsWithUsage}, cache-read requests: ${report.cache.requestsWithCacheRead}, hit rate: ${report.cache.requestHitRate === null ? "-" : `${(report.cache.requestHitRate * 100).toFixed(1)}%`}`);
|
|
63
|
+
logger.always(` Tokens: ${report.cache.cacheReadTokens} read, ${report.cache.cacheCreationTokens} created, ${report.cache.inputTokens} input`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
logger.always(chalk.yellow(" Cache usage: unavailable"));
|
|
67
|
+
}
|
|
68
|
+
logger.always("");
|
|
69
|
+
logger.always(chalk.bold(" Data Quality"));
|
|
70
|
+
logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates`);
|
|
71
|
+
if (report.accounts.length > 0) {
|
|
72
|
+
logger.always("");
|
|
73
|
+
logger.always(chalk.bold(" Accounts"));
|
|
74
|
+
const header = `${"ACCOUNT".padEnd(30)} ${"AUTH".padEnd(8)} ${"ATTEMPT".padStart(8)} ${"ATT ERR".padStart(7)} ${"FINAL".padStart(7)} ${"FIN ERR".padStart(7)} ${"QUOTA".padStart(7)} ${"TRANS".padStart(7)} ${"UNKNOWN".padStart(8)}`;
|
|
75
|
+
logger.always(` ${chalk.gray(header)}`);
|
|
76
|
+
logger.always(` ${chalk.gray("-".repeat(header.length))}`);
|
|
77
|
+
for (const account of report.accounts) {
|
|
78
|
+
const attempt = report.coverage.attempts ? String(account.attempts) : "-";
|
|
79
|
+
const attemptErrors = report.coverage.attempts
|
|
80
|
+
? String(account.attemptErrors)
|
|
81
|
+
: "-";
|
|
82
|
+
const quota = report.coverage.attempts
|
|
83
|
+
? String(account.quotaRateLimits)
|
|
84
|
+
: "-";
|
|
85
|
+
const transient = report.coverage.attempts
|
|
86
|
+
? String(account.transientRateLimits)
|
|
87
|
+
: "-";
|
|
88
|
+
const unknown = report.coverage.attempts
|
|
89
|
+
? String(account.unclassifiedRateLimits)
|
|
90
|
+
: "-";
|
|
91
|
+
const finalRequests = report.coverage.finalRequests
|
|
92
|
+
? String(account.finalRequests)
|
|
93
|
+
: "-";
|
|
94
|
+
const finalErrors = report.coverage.finalRequests
|
|
95
|
+
? String(account.finalErrors)
|
|
96
|
+
: "-";
|
|
97
|
+
logger.always(` ${account.account.slice(0, 30).padEnd(30)} ${account.accountType.slice(0, 8).padEnd(8)} ${attempt.padStart(8)} ${attemptErrors.padStart(7)} ${finalRequests.padStart(7)} ${finalErrors.padStart(7)} ${quota.padStart(7)} ${transient.padStart(7)} ${unknown.padStart(8)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
logger.always("");
|
|
101
|
+
}
|
|
102
|
+
export const proxyAnalyzeCommand = {
|
|
103
|
+
command: "analyze",
|
|
104
|
+
describe: "Analyze local proxy reliability and latency logs",
|
|
105
|
+
builder: (yargs) => yargs
|
|
106
|
+
.option("logs-dir", {
|
|
107
|
+
type: "string",
|
|
108
|
+
alias: "logsDir",
|
|
109
|
+
description: "Proxy JSONL log directory",
|
|
110
|
+
})
|
|
111
|
+
.option("since", {
|
|
112
|
+
type: "string",
|
|
113
|
+
default: "24h",
|
|
114
|
+
description: "ISO timestamp or lookback such as 6h, 1d, or 1w",
|
|
115
|
+
})
|
|
116
|
+
.option("format", {
|
|
117
|
+
type: "string",
|
|
118
|
+
choices: ["text", "json"],
|
|
119
|
+
default: "text",
|
|
120
|
+
description: "Output format",
|
|
121
|
+
})
|
|
122
|
+
.option("quiet", {
|
|
123
|
+
type: "boolean",
|
|
124
|
+
alias: "q",
|
|
125
|
+
default: false,
|
|
126
|
+
description: "Suppress non-essential output",
|
|
127
|
+
})
|
|
128
|
+
.example("neurolink proxy analyze --since 24h", "Analyze the last 24 hours of proxy logs"),
|
|
129
|
+
handler: async (argv) => {
|
|
130
|
+
try {
|
|
131
|
+
const report = await analyzeProxyLogs({
|
|
132
|
+
logsDir: argv.logsDir,
|
|
133
|
+
since: argv.since,
|
|
134
|
+
});
|
|
135
|
+
if (argv.format === "json") {
|
|
136
|
+
logger.always(JSON.stringify(report, null, 2));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
printAnalysis(report);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=proxyAnalyze.js.map
|