@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +333 -336
  3. package/dist/cli/commands/proxy.d.ts +1 -1
  4. package/dist/cli/commands/proxy.js +124 -56
  5. package/dist/cli/commands/proxyAnalyze.js +10 -1
  6. package/dist/lib/proxy/proxyAnalysis.js +136 -12
  7. package/dist/lib/proxy/requestLogger.d.ts +2 -0
  8. package/dist/lib/proxy/requestLogger.js +72 -13
  9. package/dist/lib/proxy/rollingProxyServer.js +79 -8
  10. package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
  11. package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
  12. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
  14. package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
  15. package/dist/lib/proxy/runtimeConfig.js +20 -5
  16. package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
  17. package/dist/lib/server/routes/claudeProxyRoutes.js +63 -75
  18. package/dist/lib/types/proxy.d.ts +31 -1
  19. package/dist/proxy/proxyAnalysis.js +136 -12
  20. package/dist/proxy/requestLogger.d.ts +2 -0
  21. package/dist/proxy/requestLogger.js +72 -13
  22. package/dist/proxy/rollingProxyServer.js +79 -8
  23. package/dist/proxy/rollingWorkerProcess.js +20 -15
  24. package/dist/proxy/rollingWorkerProtocol.js +3 -0
  25. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  26. package/dist/proxy/rollingWorkerSupervisor.js +31 -11
  27. package/dist/proxy/runtimeConfig.d.ts +1 -0
  28. package/dist/proxy/runtimeConfig.js +20 -5
  29. package/dist/proxy/socketWorkerRuntime.js +41 -13
  30. package/dist/server/routes/claudeProxyRoutes.js +63 -75
  31. package/dist/types/proxy.d.ts +31 -1
  32. 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
- * ("neurolink" + "proxy" both present) is not airtight, since an
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
- * ("neurolink" + "proxy" both present) is not airtight, since an
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 both "neurolink" AND
219
- // the "proxy" subcommand present in args.
220
- if (!(/neurolink/i.test(args) && /\bproxy\b/i.test(args))) {
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: Object.values(stats.accounts).map((account) => {
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 [usageStatsFlushResult, lifecycleFlushResult] = await Promise.allSettled([
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
- persistRuntimeConfig(runtimeConfigStore.getSnapshot());
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 activeHost = state?.host ?? supervisorState?.host ?? null;
2650
- const activePort = state?.port ?? supervisorState?.port ?? null;
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 = state
2656
- ? state.passthrough
2720
+ status.mode = servingState
2721
+ ? servingState.passthrough
2657
2722
  ? "passthrough"
2658
2723
  : "full"
2659
2724
  : null;
2660
- status.strategy = state?.strategy ?? null;
2725
+ status.strategy = servingState?.strategy ?? null;
2661
2726
  status.startTime =
2662
- state?.startTime ?? supervisorState?.startTime ?? null;
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 = state?.envFile ?? null;
2671
- status.fallbackChain = state?.fallbackChain ?? null;
2672
- status.accountAllowlist = state?.accountAllowlist ?? null;
2673
- status.configGeneration = state?.configGeneration ?? null;
2674
- status.configLoadedAt = state?.configLoadedAt ?? null;
2675
- status.lastConfigReloadError = state?.lastConfigReloadError ?? null;
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 ?? state?.updaterPid ?? null;
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 = Date.parse(String(record.timestamp ?? ""));
220
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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 = Date.parse(String(record.timestamp ?? ""));
302
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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 = Date.parse(String(record.timestamp ?? ""));
364
- if (!Number.isFinite(timestamp) || timestamp < sinceMs) {
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: lifecycleFiles.length > 0,
406
- finalRequests: requestFiles.length > 0,
407
- attempts: attemptFiles.length > 0,
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
  /**