@juspay/neurolink 10.4.2 → 10.4.3
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/browser/neurolink.min.js +333 -336
- package/dist/cli/commands/proxy.d.ts +1 -1
- package/dist/cli/commands/proxy.js +124 -56
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/lib/proxy/proxyAnalysis.js +136 -12
- package/dist/lib/proxy/requestLogger.d.ts +2 -0
- package/dist/lib/proxy/requestLogger.js +72 -13
- package/dist/lib/proxy/rollingProxyServer.js +79 -8
- package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
- package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
- package/dist/lib/proxy/runtimeConfig.js +20 -5
- package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
- package/dist/lib/server/routes/claudeProxyRoutes.js +63 -75
- package/dist/lib/types/proxy.d.ts +31 -1
- package/dist/proxy/proxyAnalysis.js +136 -12
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +72 -13
- package/dist/proxy/rollingProxyServer.js +79 -8
- package/dist/proxy/rollingWorkerProcess.js +20 -15
- package/dist/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/proxy/runtimeConfig.d.ts +1 -0
- package/dist/proxy/runtimeConfig.js +20 -5
- package/dist/proxy/socketWorkerRuntime.js +41 -13
- package/dist/server/routes/claudeProxyRoutes.js +63 -75
- package/dist/types/proxy.d.ts +31 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
|
|
|
22
22
|
* string) recorded when the supervisor we expect at `pid` was launched.
|
|
23
23
|
* When provided, this is cross-checked against `pid`'s actual OS-reported
|
|
24
24
|
* start time (see {@link getProcessStartTime}) — the args match alone
|
|
25
|
-
* (
|
|
25
|
+
* (the `neurolink proxy start` command is present) is not airtight, since an
|
|
26
26
|
* unrelated process (e.g. a shell running this very test suite, or a
|
|
27
27
|
* coincidentally-named script) could match it too. A pid recycled by such
|
|
28
28
|
* a process would have a start time far from the recorded supervisor
|
|
@@ -187,7 +187,7 @@ async function getProcessStartTime(pid) {
|
|
|
187
187
|
* string) recorded when the supervisor we expect at `pid` was launched.
|
|
188
188
|
* When provided, this is cross-checked against `pid`'s actual OS-reported
|
|
189
189
|
* start time (see {@link getProcessStartTime}) — the args match alone
|
|
190
|
-
* (
|
|
190
|
+
* (the `neurolink proxy start` command is present) is not airtight, since an
|
|
191
191
|
* unrelated process (e.g. a shell running this very test suite, or a
|
|
192
192
|
* coincidentally-named script) could match it too. A pid recycled by such
|
|
193
193
|
* a process would have a start time far from the recorded supervisor
|
|
@@ -215,9 +215,9 @@ export async function processLooksLikeProxySupervisor(pid, expectedStartTimeIso)
|
|
|
215
215
|
// "neurolink", an editor with a neurolink file open) would match, and a
|
|
216
216
|
// stale/recycled pid running such a process could then be SIGTERM'd/
|
|
217
217
|
// SIGKILL'd by `ensureSupervisorStoppedBeforeClear` during uninstall.
|
|
218
|
-
// Require the actual proxy-supervisor invocation
|
|
219
|
-
//
|
|
220
|
-
if (
|
|
218
|
+
// Require the actual proxy-supervisor invocation. Commands such as
|
|
219
|
+
// `neurolink proxy status` must never be mistaken for the listener owner.
|
|
220
|
+
if (!/\bneurolink(?:-proxy)?\b.*\bproxy\s+start\b/i.test(args)) {
|
|
221
221
|
return false;
|
|
222
222
|
}
|
|
223
223
|
if (!expectedStartTimeIso) {
|
|
@@ -1556,6 +1556,80 @@ export async function createProxyStartApp(params) {
|
|
|
1556
1556
|
});
|
|
1557
1557
|
const primaryAccount = await resolveStatusPrimaryAccount(activeProxyConfig);
|
|
1558
1558
|
const activeUpdaterPid = supervisorState?.updaterPid ?? runtimeState?.updaterPid;
|
|
1559
|
+
const accountRows = Object.values(stats.accounts).map((account) => {
|
|
1560
|
+
const normalizedKey = normalizeAnthropicAccountKey(account.label);
|
|
1561
|
+
const isLegacyAccount = account.type === "oauth" && account.label === legacyAccountLabel;
|
|
1562
|
+
const accountKey = storedAccountKeys.has(normalizedKey)
|
|
1563
|
+
? normalizedKey
|
|
1564
|
+
: isLegacyAccount
|
|
1565
|
+
? LEGACY_ANTHROPIC_ACCOUNT_KEY
|
|
1566
|
+
: account.label === "env"
|
|
1567
|
+
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1568
|
+
: normalizedKey;
|
|
1569
|
+
const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
|
|
1570
|
+
const cooling = (cooldowns[accountKey]?.coolingUntil ?? 0) > now;
|
|
1571
|
+
const accountStatus = disabledAccountKeys.has(accountKey)
|
|
1572
|
+
? "disabled"
|
|
1573
|
+
: !isAccountAllowed(accountKey, activeAccountAllowlist)
|
|
1574
|
+
? "excluded"
|
|
1575
|
+
: accountInventoryLoaded && account.type === "oauth" && !isStored
|
|
1576
|
+
? "removed"
|
|
1577
|
+
: cooling
|
|
1578
|
+
? "cooling"
|
|
1579
|
+
: "active";
|
|
1580
|
+
return {
|
|
1581
|
+
label: account.label,
|
|
1582
|
+
type: account.type,
|
|
1583
|
+
attempts: account.attemptCount,
|
|
1584
|
+
requests: account.successCount + account.errorCount,
|
|
1585
|
+
success: account.successCount,
|
|
1586
|
+
errors: account.errorCount,
|
|
1587
|
+
attemptErrors: account.attemptErrorCount,
|
|
1588
|
+
rateLimits: account.rateLimitCount,
|
|
1589
|
+
transientRateLimits: account.transientRateLimitCount,
|
|
1590
|
+
quotaRateLimits: account.quotaRateLimitCount,
|
|
1591
|
+
cooling,
|
|
1592
|
+
status: accountStatus,
|
|
1593
|
+
};
|
|
1594
|
+
});
|
|
1595
|
+
const attributed = accountRows.reduce((total, account) => ({
|
|
1596
|
+
attempts: total.attempts + (account.attempts ?? 0),
|
|
1597
|
+
requests: total.requests + (account.requests ?? 0),
|
|
1598
|
+
success: total.success + (account.success ?? 0),
|
|
1599
|
+
errors: total.errors + (account.errors ?? 0),
|
|
1600
|
+
attemptErrors: total.attemptErrors + (account.attemptErrors ?? 0),
|
|
1601
|
+
rateLimits: total.rateLimits + (account.rateLimits ?? 0),
|
|
1602
|
+
transientRateLimits: total.transientRateLimits + (account.transientRateLimits ?? 0),
|
|
1603
|
+
quotaRateLimits: total.quotaRateLimits + (account.quotaRateLimits ?? 0),
|
|
1604
|
+
}), {
|
|
1605
|
+
attempts: 0,
|
|
1606
|
+
requests: 0,
|
|
1607
|
+
success: 0,
|
|
1608
|
+
errors: 0,
|
|
1609
|
+
attemptErrors: 0,
|
|
1610
|
+
rateLimits: 0,
|
|
1611
|
+
transientRateLimits: 0,
|
|
1612
|
+
quotaRateLimits: 0,
|
|
1613
|
+
});
|
|
1614
|
+
const unattributed = {
|
|
1615
|
+
attempts: Math.max(0, stats.totalAttempts - attributed.attempts),
|
|
1616
|
+
requests: Math.max(0, stats.totalRequests - attributed.requests),
|
|
1617
|
+
success: Math.max(0, stats.totalSuccess - attributed.success),
|
|
1618
|
+
errors: Math.max(0, stats.totalErrors - attributed.errors),
|
|
1619
|
+
attemptErrors: Math.max(0, stats.totalAttemptErrors - attributed.attemptErrors),
|
|
1620
|
+
rateLimits: Math.max(0, stats.totalRateLimits - attributed.rateLimits),
|
|
1621
|
+
transientRateLimits: Math.max(0, stats.totalTransientRateLimits - attributed.transientRateLimits),
|
|
1622
|
+
quotaRateLimits: Math.max(0, stats.totalQuotaRateLimits - attributed.quotaRateLimits),
|
|
1623
|
+
};
|
|
1624
|
+
if (Object.values(unattributed).some((count) => count > 0)) {
|
|
1625
|
+
accountRows.push({
|
|
1626
|
+
label: "unattributed",
|
|
1627
|
+
type: "internal",
|
|
1628
|
+
...unattributed,
|
|
1629
|
+
cooling: false,
|
|
1630
|
+
status: "unattributed",
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1559
1633
|
return c.json({
|
|
1560
1634
|
status: "running",
|
|
1561
1635
|
ready: health.ready,
|
|
@@ -1578,42 +1652,7 @@ export async function createProxyStartApp(params) {
|
|
|
1578
1652
|
totalRateLimits: stats.totalRateLimits,
|
|
1579
1653
|
totalTransientRateLimits: stats.totalTransientRateLimits,
|
|
1580
1654
|
totalQuotaRateLimits: stats.totalQuotaRateLimits,
|
|
1581
|
-
accounts:
|
|
1582
|
-
const normalizedKey = normalizeAnthropicAccountKey(account.label);
|
|
1583
|
-
const isLegacyAccount = account.type === "oauth" && account.label === legacyAccountLabel;
|
|
1584
|
-
const accountKey = storedAccountKeys.has(normalizedKey)
|
|
1585
|
-
? normalizedKey
|
|
1586
|
-
: isLegacyAccount
|
|
1587
|
-
? LEGACY_ANTHROPIC_ACCOUNT_KEY
|
|
1588
|
-
: account.label === "env"
|
|
1589
|
-
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1590
|
-
: normalizedKey;
|
|
1591
|
-
const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
|
|
1592
|
-
const cooling = (cooldowns[accountKey]?.coolingUntil ?? 0) > now;
|
|
1593
|
-
const accountStatus = disabledAccountKeys.has(accountKey)
|
|
1594
|
-
? "disabled"
|
|
1595
|
-
: !isAccountAllowed(accountKey, activeAccountAllowlist)
|
|
1596
|
-
? "excluded"
|
|
1597
|
-
: accountInventoryLoaded && account.type === "oauth" && !isStored
|
|
1598
|
-
? "removed"
|
|
1599
|
-
: cooling
|
|
1600
|
-
? "cooling"
|
|
1601
|
-
: "active";
|
|
1602
|
-
return {
|
|
1603
|
-
label: account.label,
|
|
1604
|
-
type: account.type,
|
|
1605
|
-
attempts: account.attemptCount,
|
|
1606
|
-
requests: account.successCount + account.errorCount,
|
|
1607
|
-
success: account.successCount,
|
|
1608
|
-
errors: account.errorCount,
|
|
1609
|
-
attemptErrors: account.attemptErrorCount,
|
|
1610
|
-
rateLimits: account.rateLimitCount,
|
|
1611
|
-
transientRateLimits: account.transientRateLimitCount,
|
|
1612
|
-
quotaRateLimits: account.quotaRateLimitCount,
|
|
1613
|
-
cooling,
|
|
1614
|
-
status: accountStatus,
|
|
1615
|
-
};
|
|
1616
|
-
}),
|
|
1655
|
+
accounts: accountRows,
|
|
1617
1656
|
primaryAccount,
|
|
1618
1657
|
persistence: getUsageStatsPersistenceStatus(),
|
|
1619
1658
|
},
|
|
@@ -1930,9 +1969,11 @@ function registerProxyShutdownHandlers(params) {
|
|
|
1930
1969
|
}
|
|
1931
1970
|
}
|
|
1932
1971
|
const usageStatsFlush = import("../../lib/proxy/usageStats.js").then(({ flushUsageStats }) => flushUsageStats());
|
|
1933
|
-
const
|
|
1972
|
+
const requestLogsFlush = import("../../lib/proxy/requestLogger.js").then(({ flushRequestLogs }) => flushRequestLogs());
|
|
1973
|
+
const [usageStatsFlushResult, lifecycleFlushResult, requestLogsFlushResult,] = await Promise.allSettled([
|
|
1934
1974
|
withTimeout(usageStatsFlush, PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy usage statistics during shutdown"),
|
|
1935
1975
|
withTimeout(flushProxyLifecycleEvents(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy lifecycle metadata during shutdown"),
|
|
1976
|
+
withTimeout(requestLogsFlush, PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy request logs during shutdown"),
|
|
1936
1977
|
]);
|
|
1937
1978
|
if (usageStatsFlushResult.status === "rejected") {
|
|
1938
1979
|
const error = usageStatsFlushResult.reason;
|
|
@@ -1942,6 +1983,10 @@ function registerProxyShutdownHandlers(params) {
|
|
|
1942
1983
|
const error = lifecycleFlushResult.reason;
|
|
1943
1984
|
logger.debug(`[proxy] lifecycle metadata flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1944
1985
|
}
|
|
1986
|
+
if (requestLogsFlushResult.status === "rejected") {
|
|
1987
|
+
const error = requestLogsFlushResult.reason;
|
|
1988
|
+
logger.warn(`[proxy] request log flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1989
|
+
}
|
|
1945
1990
|
try {
|
|
1946
1991
|
const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../lib/services/server/ai/observability/instrumentation.js");
|
|
1947
1992
|
await flushOpenTelemetry();
|
|
@@ -2136,8 +2181,27 @@ async function startProxyRuntime(params) {
|
|
|
2136
2181
|
let stopRuntimeConfig;
|
|
2137
2182
|
if (params.runtimeConfigStore) {
|
|
2138
2183
|
const runtimeConfigStore = params.runtimeConfigStore;
|
|
2139
|
-
const unsubscribeReload = runtimeConfigStore.subscribeReload(() => {
|
|
2140
|
-
|
|
2184
|
+
const unsubscribeReload = runtimeConfigStore.subscribeReload((result) => {
|
|
2185
|
+
const snapshot = runtimeConfigStore.getSnapshot();
|
|
2186
|
+
persistRuntimeConfig(snapshot);
|
|
2187
|
+
if (socketWorker &&
|
|
2188
|
+
result.applied &&
|
|
2189
|
+
result.changed &&
|
|
2190
|
+
result.environmentChanged &&
|
|
2191
|
+
process.connected &&
|
|
2192
|
+
process.send) {
|
|
2193
|
+
try {
|
|
2194
|
+
process.send({
|
|
2195
|
+
type: "proxy-worker:replacement-requested",
|
|
2196
|
+
generation: getProxyWorkerGeneration(),
|
|
2197
|
+
pid: process.pid,
|
|
2198
|
+
reason: "environment",
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
catch (error) {
|
|
2202
|
+
logger.warn(`[proxy] failed to request environment replacement: ${error instanceof Error ? error.message : String(error)}`);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2141
2205
|
});
|
|
2142
2206
|
const reloadOnSighup = () => {
|
|
2143
2207
|
void runtimeConfigStore.reload("sighup");
|
|
@@ -2646,20 +2710,23 @@ export const proxyStatusCommand = {
|
|
|
2646
2710
|
supervisorState.rolling.active?.pid === state?.pid)
|
|
2647
2711
|
: true);
|
|
2648
2712
|
if ((state || supervisorState) && (servingWorker || supervisorRunning)) {
|
|
2649
|
-
const
|
|
2650
|
-
const
|
|
2713
|
+
const servingState = servingWorker ? state : null;
|
|
2714
|
+
const activeHost = servingState?.host ?? supervisorState?.host ?? null;
|
|
2715
|
+
const activePort = servingState?.port ?? supervisorState?.port ?? null;
|
|
2651
2716
|
status.running = true;
|
|
2652
2717
|
status.pid = servingWorker && state ? state.pid : null;
|
|
2653
2718
|
status.port = activePort;
|
|
2654
2719
|
status.host = activeHost;
|
|
2655
|
-
status.mode =
|
|
2656
|
-
?
|
|
2720
|
+
status.mode = servingState
|
|
2721
|
+
? servingState.passthrough
|
|
2657
2722
|
? "passthrough"
|
|
2658
2723
|
: "full"
|
|
2659
2724
|
: null;
|
|
2660
|
-
status.strategy =
|
|
2725
|
+
status.strategy = servingState?.strategy ?? null;
|
|
2661
2726
|
status.startTime =
|
|
2662
|
-
|
|
2727
|
+
(supervisorRunning ? supervisorState?.startTime : null) ??
|
|
2728
|
+
servingState?.startTime ??
|
|
2729
|
+
null;
|
|
2663
2730
|
status.uptime = status.startTime
|
|
2664
2731
|
? Date.now() - new Date(status.startTime).getTime()
|
|
2665
2732
|
: null;
|
|
@@ -2667,17 +2734,18 @@ export const proxyStatusCommand = {
|
|
|
2667
2734
|
activeHost && activePort
|
|
2668
2735
|
? `http://${activeHost === "0.0.0.0" ? "localhost" : activeHost}:${activePort}`
|
|
2669
2736
|
: null;
|
|
2670
|
-
status.envFile =
|
|
2671
|
-
status.fallbackChain =
|
|
2672
|
-
status.accountAllowlist =
|
|
2673
|
-
status.configGeneration =
|
|
2674
|
-
status.configLoadedAt =
|
|
2675
|
-
status.lastConfigReloadError =
|
|
2737
|
+
status.envFile = servingState?.envFile ?? null;
|
|
2738
|
+
status.fallbackChain = servingState?.fallbackChain ?? null;
|
|
2739
|
+
status.accountAllowlist = servingState?.accountAllowlist ?? null;
|
|
2740
|
+
status.configGeneration = servingState?.configGeneration ?? null;
|
|
2741
|
+
status.configLoadedAt = servingState?.configLoadedAt ?? null;
|
|
2742
|
+
status.lastConfigReloadError =
|
|
2743
|
+
servingState?.lastConfigReloadError ?? null;
|
|
2676
2744
|
status.supervisorPid = supervisorPid ?? null;
|
|
2677
2745
|
status.supervisorRunning = supervisorRunning;
|
|
2678
2746
|
status.rolling = supervisorState?.rolling ?? null;
|
|
2679
2747
|
status.updaterPid =
|
|
2680
|
-
supervisorState?.updaterPid ??
|
|
2748
|
+
supervisorState?.updaterPid ?? servingState?.updaterPid ?? null;
|
|
2681
2749
|
status.updaterRunning = status.updaterPid
|
|
2682
2750
|
? isProcessRunning(status.updaterPid)
|
|
2683
2751
|
: false;
|
|
@@ -11,7 +11,7 @@ function printAnalysis(report) {
|
|
|
11
11
|
logger.always(chalk.gray("=".repeat(50)));
|
|
12
12
|
logger.always(` Since: ${chalk.cyan(report.since)}`);
|
|
13
13
|
logger.always(` Logs: ${chalk.cyan(report.logsDir)}`);
|
|
14
|
-
logger.always(` Files: ${report.files.requests} request, ${report.files.attempts} attempt, ${report.files.lifecycle} lifecycle`);
|
|
14
|
+
logger.always(` Files: ${report.files.requests} request, ${report.files.attempts} attempt, ${report.files.lifecycle} lifecycle, ${report.files.debug} debug`);
|
|
15
15
|
logger.always("");
|
|
16
16
|
logger.always(chalk.bold(" Reliability"));
|
|
17
17
|
logger.always(report.coverage.finalRequests
|
|
@@ -68,6 +68,15 @@ function printAnalysis(report) {
|
|
|
68
68
|
logger.always("");
|
|
69
69
|
logger.always(chalk.bold(" Data Quality"));
|
|
70
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
|
+
for (const [stream, range] of Object.entries(report.dataQuality.streams)) {
|
|
72
|
+
if (range.observedFrom) {
|
|
73
|
+
logger.always(` ${stream}: ${range.observedFrom} to ${range.observedTo}${range.startsAtOrBeforeRequestedWindow ? "" : chalk.yellow(" (starts after requested window)")}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const artifacts = report.dataQuality.bodyArtifacts;
|
|
77
|
+
if (artifacts.capturesIndexed > 0) {
|
|
78
|
+
logger.always(` Body artifacts: ${artifacts.artifactsPresent}/${artifacts.artifactsReferenced} present, ${artifacts.artifactsMissing} missing, ${artifacts.writeFailures} write failures, ${artifacts.invalidPaths} invalid paths, ${artifacts.truncatedCaptures} truncated captures`);
|
|
79
|
+
}
|
|
71
80
|
if (report.accounts.length > 0) {
|
|
72
81
|
logger.always("");
|
|
73
82
|
logger.always(chalk.bold(" Accounts"));
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
7
7
|
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
8
8
|
const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
9
|
+
const DEBUG_FILE_PATTERN = /^proxy-debug-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
10
|
+
const ARTIFACT_STAT_CONCURRENCY = 64;
|
|
9
11
|
const LIFECYCLE_EVENTS = new Set([
|
|
10
12
|
"request_accepted",
|
|
11
13
|
"response_headers",
|
|
12
14
|
"response_first_chunk",
|
|
13
15
|
"request_terminal",
|
|
14
16
|
]);
|
|
17
|
+
function isContainedPath(root, candidate) {
|
|
18
|
+
const candidateRelative = relative(root, candidate);
|
|
19
|
+
return (candidateRelative.length > 0 &&
|
|
20
|
+
!isAbsolute(candidateRelative) &&
|
|
21
|
+
candidateRelative !== ".." &&
|
|
22
|
+
!candidateRelative.startsWith(`..${sep}`));
|
|
23
|
+
}
|
|
24
|
+
async function inspectBodyArtifact(artifactPath, canonicalBodiesRoot) {
|
|
25
|
+
if (!canonicalBodiesRoot) {
|
|
26
|
+
return "missing";
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const canonicalArtifactPath = await realpath(artifactPath);
|
|
30
|
+
if (!isContainedPath(canonicalBodiesRoot, canonicalArtifactPath)) {
|
|
31
|
+
return "invalid";
|
|
32
|
+
}
|
|
33
|
+
return (await stat(canonicalArtifactPath)).isFile() ? "present" : "missing";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "missing";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
15
39
|
function increment(counter, key) {
|
|
16
40
|
counter[key] = (counter[key] ?? 0) + 1;
|
|
17
41
|
}
|
|
@@ -192,6 +216,7 @@ async function discoverLogFiles(logsDir) {
|
|
|
192
216
|
lifecycleFiles: matching(LIFECYCLE_FILE_PATTERN),
|
|
193
217
|
requestFiles: matching(REQUEST_FILE_PATTERN),
|
|
194
218
|
attemptFiles: matching(ATTEMPT_FILE_PATTERN),
|
|
219
|
+
debugFiles: matching(DEBUG_FILE_PATTERN),
|
|
195
220
|
};
|
|
196
221
|
}
|
|
197
222
|
/** Analyze local proxy logs without reading request or response body artifacts. */
|
|
@@ -199,7 +224,24 @@ export async function analyzeProxyLogs(options) {
|
|
|
199
224
|
const nowMs = options?.nowMs ?? Date.now();
|
|
200
225
|
const sinceMs = parseSince(options?.since ?? "24h", nowMs);
|
|
201
226
|
const logsDir = resolve(options?.logsDir ?? join(homedir(), ".neurolink", "logs"));
|
|
202
|
-
const { lifecycleFiles, requestFiles, attemptFiles } = await discoverLogFiles(logsDir);
|
|
227
|
+
const { lifecycleFiles, requestFiles, attemptFiles, debugFiles } = await discoverLogFiles(logsDir);
|
|
228
|
+
const observedRanges = {
|
|
229
|
+
lifecycle: { from: null, to: null },
|
|
230
|
+
requests: { from: null, to: null },
|
|
231
|
+
attempts: { from: null, to: null },
|
|
232
|
+
debug: { from: null, to: null },
|
|
233
|
+
};
|
|
234
|
+
const observeTimestamp = (stream, record) => {
|
|
235
|
+
const timestamp = Date.parse(String(record.timestamp ?? ""));
|
|
236
|
+
if (!Number.isFinite(timestamp)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const range = observedRanges[stream];
|
|
240
|
+
range.from =
|
|
241
|
+
range.from === null ? timestamp : Math.min(range.from, timestamp);
|
|
242
|
+
range.to = range.to === null ? timestamp : Math.max(range.to, timestamp);
|
|
243
|
+
return timestamp;
|
|
244
|
+
};
|
|
203
245
|
let linesRead = 0;
|
|
204
246
|
let malformedLines = 0;
|
|
205
247
|
let unsupportedLifecycleLines = 0;
|
|
@@ -216,8 +258,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
216
258
|
const sequences = new Map();
|
|
217
259
|
for (const filePath of lifecycleFiles) {
|
|
218
260
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
219
|
-
const timestamp =
|
|
220
|
-
if (
|
|
261
|
+
const timestamp = observeTimestamp("lifecycle", record);
|
|
262
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
221
263
|
return;
|
|
222
264
|
}
|
|
223
265
|
const event = stringValue(record.event);
|
|
@@ -298,8 +340,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
298
340
|
let unclassifiedRateLimits = 0;
|
|
299
341
|
for (const filePath of attemptFiles) {
|
|
300
342
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
301
|
-
const timestamp =
|
|
302
|
-
if (
|
|
343
|
+
const timestamp = observeTimestamp("attempts", record);
|
|
344
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
303
345
|
return;
|
|
304
346
|
}
|
|
305
347
|
const requestId = stringValue(record.requestId);
|
|
@@ -360,8 +402,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
360
402
|
const terminalStreamErrors = new Set();
|
|
361
403
|
for (const filePath of requestFiles) {
|
|
362
404
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
363
|
-
const timestamp =
|
|
364
|
-
if (
|
|
405
|
+
const timestamp = observeTimestamp("requests", record);
|
|
406
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
365
407
|
return;
|
|
366
408
|
}
|
|
367
409
|
const requestId = stringValue(record.requestId);
|
|
@@ -391,6 +433,70 @@ export async function analyzeProxyLogs(options) {
|
|
|
391
433
|
malformedLines += 1;
|
|
392
434
|
});
|
|
393
435
|
}
|
|
436
|
+
let capturesIndexed = 0;
|
|
437
|
+
let truncatedCaptures = 0;
|
|
438
|
+
let writeFailures = 0;
|
|
439
|
+
let invalidPaths = 0;
|
|
440
|
+
const referencedArtifacts = new Set();
|
|
441
|
+
const bodiesRoot = resolve(logsDir, "bodies");
|
|
442
|
+
for (const filePath of debugFiles) {
|
|
443
|
+
linesRead += await readJsonLines(filePath, (record) => {
|
|
444
|
+
const timestamp = observeTimestamp("debug", record);
|
|
445
|
+
if (timestamp === null || timestamp < sinceMs) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (record.type !== "body_capture") {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
capturesIndexed += 1;
|
|
452
|
+
truncatedCaptures += record.bodyTruncated === true ? 1 : 0;
|
|
453
|
+
writeFailures += record.bodyWriteFailed === true ? 1 : 0;
|
|
454
|
+
const bodyPath = stringValue(record.bodyPath);
|
|
455
|
+
if (!bodyPath) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (bodyPath.includes("\0") || bodyPath.split(/[\\/]/).includes("..")) {
|
|
459
|
+
invalidPaths += 1;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const resolvedBodyPath = resolve(logsDir, bodyPath);
|
|
463
|
+
if (!isContainedPath(bodiesRoot, resolvedBodyPath)) {
|
|
464
|
+
invalidPaths += 1;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
referencedArtifacts.add(resolvedBodyPath);
|
|
468
|
+
}, () => {
|
|
469
|
+
malformedLines += 1;
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const artifactPaths = [...referencedArtifacts];
|
|
473
|
+
let canonicalBodiesRoot = null;
|
|
474
|
+
let bodiesRootUnsafe = false;
|
|
475
|
+
try {
|
|
476
|
+
const bodiesRootStat = await lstat(bodiesRoot);
|
|
477
|
+
if (bodiesRootStat.isDirectory() && !bodiesRootStat.isSymbolicLink()) {
|
|
478
|
+
canonicalBodiesRoot = await realpath(bodiesRoot);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
bodiesRootUnsafe = true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// A missing body directory means every lexically valid reference is absent.
|
|
486
|
+
}
|
|
487
|
+
let artifactsPresent = 0;
|
|
488
|
+
let artifactsMissing = 0;
|
|
489
|
+
for (let offset = 0; offset < artifactPaths.length; offset += ARTIFACT_STAT_CONCURRENCY) {
|
|
490
|
+
const presence = await Promise.all(artifactPaths
|
|
491
|
+
.slice(offset, offset + ARTIFACT_STAT_CONCURRENCY)
|
|
492
|
+
.map((artifactPath) => bodiesRootUnsafe
|
|
493
|
+
? Promise.resolve("invalid")
|
|
494
|
+
: inspectBodyArtifact(artifactPath, canonicalBodiesRoot)));
|
|
495
|
+
artifactsPresent += presence.filter((value) => value === "present").length;
|
|
496
|
+
artifactsMissing += presence.filter((value) => value === "missing").length;
|
|
497
|
+
invalidPaths += presence.filter((value) => value === "invalid").length;
|
|
498
|
+
}
|
|
499
|
+
const artifactsReferenced = artifactsPresent + artifactsMissing;
|
|
394
500
|
const finalSummary = summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByRequest, accounts);
|
|
395
501
|
return {
|
|
396
502
|
generatedAt: new Date(nowMs).toISOString(),
|
|
@@ -400,11 +506,12 @@ export async function analyzeProxyLogs(options) {
|
|
|
400
506
|
lifecycle: lifecycleFiles.length,
|
|
401
507
|
requests: requestFiles.length,
|
|
402
508
|
attempts: attemptFiles.length,
|
|
509
|
+
debug: debugFiles.length,
|
|
403
510
|
},
|
|
404
511
|
coverage: {
|
|
405
|
-
lifecycle:
|
|
406
|
-
finalRequests:
|
|
407
|
-
attempts:
|
|
512
|
+
lifecycle: accepted.size + headers.size + firstChunks.size + terminal.size > 0,
|
|
513
|
+
finalRequests: finalRequests.size > 0 || terminalStreamErrors.size > 0,
|
|
514
|
+
attempts: totalAttempts > 0,
|
|
408
515
|
attemptLatency: attemptLatency.length > 0,
|
|
409
516
|
cacheUsage: finalSummary.cache.requestsWithUsage > 0,
|
|
410
517
|
},
|
|
@@ -414,6 +521,23 @@ export async function analyzeProxyLogs(options) {
|
|
|
414
521
|
unsupportedLifecycleLines,
|
|
415
522
|
lifecycleSequenceGaps,
|
|
416
523
|
lifecycleSequenceDuplicates,
|
|
524
|
+
streams: Object.fromEntries(Object.entries(observedRanges).map(([stream, range]) => [
|
|
525
|
+
stream,
|
|
526
|
+
{
|
|
527
|
+
observedFrom: range.from === null ? null : new Date(range.from).toISOString(),
|
|
528
|
+
observedTo: range.to === null ? null : new Date(range.to).toISOString(),
|
|
529
|
+
startsAtOrBeforeRequestedWindow: range.from !== null && range.from <= sinceMs,
|
|
530
|
+
},
|
|
531
|
+
])),
|
|
532
|
+
bodyArtifacts: {
|
|
533
|
+
capturesIndexed,
|
|
534
|
+
artifactsReferenced,
|
|
535
|
+
artifactsPresent,
|
|
536
|
+
artifactsMissing,
|
|
537
|
+
invalidPaths,
|
|
538
|
+
writeFailures,
|
|
539
|
+
truncatedCaptures,
|
|
540
|
+
},
|
|
417
541
|
},
|
|
418
542
|
lifecycle: {
|
|
419
543
|
accepted: accepted.size,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Useful for debugging and auditing proxy traffic.
|
|
7
7
|
*/
|
|
8
8
|
import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry } from "../types/index.js";
|
|
9
|
+
/** Wait, up to a bounded deadline, for admitted request/body writes to settle. */
|
|
10
|
+
export declare function flushRequestLogs(timeoutMs?: number): Promise<void>;
|
|
9
11
|
export declare function initRequestLogger(enabled?: boolean, customLogsDir?: string): void;
|
|
10
12
|
export declare function logRequest(entry: RequestLogEntry): Promise<void>;
|
|
11
13
|
/**
|