@juspay/neurolink 12.20.0 → 12.20.1

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 (33) hide show
  1. package/CHANGELOG.md +2 -6
  2. package/dist/browser/neurolink.min.js +386 -386
  3. package/dist/cli/commands/proxy.js +171 -25
  4. package/dist/cli/commands/telemetry.d.ts +4 -1
  5. package/dist/cli/commands/telemetry.js +113 -0
  6. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  7. package/dist/cli/utils/nativeTelemetry.js +322 -0
  8. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  9. package/dist/proxy/bodyCapturePolicy.js +132 -0
  10. package/dist/proxy/otelLogSink.d.ts +11 -1
  11. package/dist/proxy/otelLogSink.js +174 -27
  12. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  13. package/dist/proxy/otlpLogTransport.js +22 -7
  14. package/dist/proxy/requestLogger.js +11 -3
  15. package/dist/proxy/restartControl.js +18 -3
  16. package/dist/proxy/rollingProxyServer.js +48 -1
  17. package/dist/proxy/rollingWorkerProcess.js +87 -10
  18. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  19. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  20. package/dist/proxy/socketWorkerRuntime.js +90 -4
  21. package/dist/proxy/updateChecker.d.ts +3 -3
  22. package/dist/proxy/updateChecker.js +16 -9
  23. package/dist/proxy/updateCoordinator.d.ts +10 -1
  24. package/dist/proxy/updateCoordinator.js +68 -1
  25. package/dist/proxy/updateState.d.ts +2 -0
  26. package/dist/proxy/updateState.js +15 -0
  27. package/dist/types/cli.d.ts +18 -0
  28. package/dist/types/proxy.d.ts +97 -3
  29. package/docs-site/static/search-index.json +1 -1
  30. package/package.json +1 -1
  31. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  32. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  33. package/scripts/observability/proxy-telemetry-check.mjs +135 -50
@@ -1,3 +1,41 @@
1
+ /**
2
+ * Parse serving identity conservatively from `/status`. A rolling status is
3
+ * trusted only when the worker-local fields agree with the supervisor snapshot.
4
+ */
5
+ export function parseProxyServingWorkerIdentity(payload, rollingSupervisor) {
6
+ if (!payload || typeof payload !== "object") {
7
+ return null;
8
+ }
9
+ const value = payload;
10
+ if (typeof value.version !== "string" ||
11
+ !/^\d+\.\d+\.\d+$/.test(value.version) ||
12
+ !Number.isSafeInteger(value.pid) ||
13
+ Number(value.pid) <= 0) {
14
+ return null;
15
+ }
16
+ if (!rollingSupervisor) {
17
+ return { version: value.version, pid: Number(value.pid), generation: null };
18
+ }
19
+ const active = value.autoUpdate?.rolling?.active;
20
+ if (!active ||
21
+ active.version !== value.version ||
22
+ active.pid !== value.pid ||
23
+ !Number.isSafeInteger(active.generation) ||
24
+ Number(active.generation) <= 0) {
25
+ return null;
26
+ }
27
+ return {
28
+ version: value.version,
29
+ pid: Number(value.pid),
30
+ generation: Number(active.generation),
31
+ };
32
+ }
33
+ /** A long-running update may mutate live selection only for its original worker. */
34
+ export function isSameProxyServingWorker(expected, current) {
35
+ return (current?.version === expected.version &&
36
+ current.pid === expected.pid &&
37
+ current.generation === expected.generation);
38
+ }
1
39
  /**
2
40
  * A replacement updater starts from the newly installed package. The registry
3
41
  * then reports no newer version even when its launchd parent still runs the
@@ -72,6 +110,8 @@ function isQuiet(activity, quietThresholdMs, nowMs) {
72
110
  }
73
111
  /**
74
112
  * Prefer a naturally quiet window, then briefly drain new inference traffic.
113
+ * Automatic supervisor refresh uses allowBusyDrain=false: it fences admission
114
+ * only after a quiet observation and never waits for busy workers behind it.
75
115
  * Existing requests are never interrupted; a bounded drain failure is returned
76
116
  * to the caller so admission can be reopened and retried later.
77
117
  */
@@ -89,16 +129,43 @@ export async function waitForProxyUpdateWindow(options) {
89
129
  }
90
130
  const activity = await options.getActivity();
91
131
  if (activity && isQuiet(activity, options.quietThresholdMs, now())) {
92
- return { ready: true, draining: false };
132
+ if (options.allowBusyDrain !== false) {
133
+ return { ready: true, draining: false };
134
+ }
135
+ break;
93
136
  }
94
137
  options.onPhase?.("waiting_for_quiet", activity);
95
138
  await wait(options.pollIntervalMs);
96
139
  }
140
+ if (options.allowBusyDrain === false) {
141
+ if (options.isStopping()) {
142
+ return { ready: false, draining: false, reason: "stopping" };
143
+ }
144
+ if (!options.isParentAlive()) {
145
+ return { ready: false, draining: false, reason: "parent_stopped" };
146
+ }
147
+ const activity = await options.getActivity();
148
+ if (!activity || !isQuiet(activity, options.quietThresholdMs, now())) {
149
+ options.onPhase?.("waiting_for_quiet", activity);
150
+ return { ready: false, draining: false, reason: "busy" };
151
+ }
152
+ }
97
153
  if (!(await options.setDraining(true))) {
98
154
  // The control response may have been lost after the parent applied it.
99
155
  // Treat state as unknown/draining so the caller always attempts resume.
100
156
  return { ready: false, draining: true, reason: "drain_failed" };
101
157
  }
158
+ if (options.allowBusyDrain === false) {
159
+ // The worker's idle fence closes the observation/admission race. Any new
160
+ // cross-generation activity means resume immediately, never hold a busy
161
+ // service behind maintenance responses for the normal drain timeout.
162
+ const activity = await options.getActivity();
163
+ if (activity && isProxyRuntimeSettled(activity)) {
164
+ return { ready: true, draining: true };
165
+ }
166
+ const resumed = await options.setDraining(false);
167
+ return { ready: false, draining: !resumed, reason: "busy" };
168
+ }
102
169
  const drainDeadline = now() + options.drainWaitMs;
103
170
  while (now() < drainDeadline) {
104
171
  if (options.isStopping()) {
@@ -67,3 +67,5 @@ export declare function reconcileRunningUpdate(runningVersion: string, stateFile
67
67
  * @param stateFilePath - Override path for testing
68
68
  */
69
69
  export declare function recordCheck(latestVersion: string, stateFilePath?: string): void;
70
+ /** Preserve the last successful registry version when discovery fails. */
71
+ export declare function recordCheckFailure(message: string, stateFilePath?: string): void;
@@ -124,6 +124,12 @@ export function loadUpdateState(stateFilePath) {
124
124
  return {
125
125
  ...getDefaultUpdateState(),
126
126
  ...candidate,
127
+ lastCheckAttemptAt: typeof candidate.lastCheckAttemptAt === "string"
128
+ ? candidate.lastCheckAttemptAt
129
+ : undefined,
130
+ lastCheckError: typeof candidate.lastCheckError === "string"
131
+ ? candidate.lastCheckError.slice(0, 1_000)
132
+ : null,
127
133
  suppressedVersions: candidate.suppressedVersions ?? {},
128
134
  // Backfill order matters for state files written before `installedVersion`
129
135
  // existed. Back then `recordUpdateInstalled()` set ONLY
@@ -303,6 +309,15 @@ export function reconcileRunningUpdate(runningVersion, stateFilePath) {
303
309
  export function recordCheck(latestVersion, stateFilePath) {
304
310
  const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
305
311
  state.lastCheckAt = new Date().toISOString();
312
+ state.lastCheckAttemptAt = state.lastCheckAt;
313
+ state.lastCheckError = null;
306
314
  state.lastCheckVersion = latestVersion;
307
315
  saveUpdateState(state, stateFilePath);
308
316
  }
317
+ /** Preserve the last successful registry version when discovery fails. */
318
+ export function recordCheckFailure(message, stateFilePath) {
319
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
320
+ state.lastCheckAttemptAt = new Date().toISOString();
321
+ state.lastCheckError = message.slice(0, 1_000);
322
+ saveUpdateState(state, stateFilePath);
323
+ }
@@ -1044,6 +1044,24 @@ export type TelemetryStatsArgs = TelemetryCommandArgs & {
1044
1044
  byModel?: boolean;
1045
1045
  byProvider?: boolean;
1046
1046
  };
1047
+ /** Staging-only native collector migration; never activates a service. */
1048
+ export type TelemetryNativePrepareArgs = {
1049
+ collectorConfig: string;
1050
+ output: string;
1051
+ queueDirectory: string;
1052
+ compactionDirectory: string;
1053
+ bodyPort: number;
1054
+ bodyStream?: string;
1055
+ metadataQueueMib: number;
1056
+ bodyQueueMib: number;
1057
+ diskQuotaMib: number;
1058
+ metadataRetentionDays: number;
1059
+ bodyRetentionDays: number;
1060
+ };
1061
+ export type TelemetryNativeValidateArgs = {
1062
+ directory: string;
1063
+ collectorBin: string;
1064
+ };
1047
1065
  /** Available exporter names */
1048
1066
  export type ExporterName = "langfuse" | "langsmith" | "otel" | "datadog" | "sentry" | "braintrust" | "arize" | "posthog" | "laminar";
1049
1067
  /** Observability command arguments */
@@ -16,6 +16,11 @@ import type { ProxyContextEvidence, ProxyPreparedContext } from "./proxyContext.
16
16
  * - src/lib/server/routes/claudeProxyRoutes.ts (runtime state, deps)
17
17
  */
18
18
  import type { Counter, Histogram, Span } from "@opentelemetry/api";
19
+ import type { LogRecordExporter } from "@opentelemetry/sdk-logs";
20
+ /** HTTP exporter with an explicit bounded-shutdown cancellation boundary. */
21
+ export type ProxyOtlpLogTransport = LogRecordExporter & {
22
+ abortPending: () => void;
23
+ };
19
24
  import type { Hono } from "hono";
20
25
  import type { Ora } from "ora";
21
26
  import type { MCPToolRegistry } from "../mcp/toolRegistry.js";
@@ -726,6 +731,49 @@ export type ProxyBodyDeliveryResult = {
726
731
  maxChunkExportMs?: number;
727
732
  reason?: string;
728
733
  };
734
+ /** One-hop reference to identical redacted bytes from the same request. */
735
+ export type ProxyBodyCaptureReference = {
736
+ captureId: string;
737
+ requestId: string;
738
+ bodySha256: string;
739
+ redactedBodyBytes: number;
740
+ /** Source export settlement time, used to bound backend reconstruction. */
741
+ exportedAt: string;
742
+ };
743
+ export type ProxyBodyCapturePublication = {
744
+ delivery?: ProxyBodyDeliveryResult | {
745
+ status: "reference";
746
+ reason: "identical_redacted_payload";
747
+ } | {
748
+ status: "policy_excluded";
749
+ reason: "body_byte_budget_exhausted" | "body_byte_budget_invalid";
750
+ };
751
+ reference?: ProxyBodyCaptureReference;
752
+ };
753
+ export type ProxyBodyCapturePolicyOptions = {
754
+ bytesPerMinute?: string;
755
+ deduplicate?: boolean;
756
+ now?: () => number;
757
+ };
758
+ export type ProxyBodyCapturePolicySnapshot = {
759
+ deduplicationEnabled: boolean;
760
+ maxReferences: number;
761
+ referenceTtlMs: number;
762
+ references: number;
763
+ byteBudgetPerMinute: number | null;
764
+ invalidByteBudget: boolean;
765
+ availableBytes: number | null;
766
+ submittedBytes: number;
767
+ deduplicatedCaptures: number;
768
+ deduplicatedBytes: number;
769
+ policyExcludedCaptures: number;
770
+ policyExcludedBytes: number;
771
+ };
772
+ export type ProxyBodyDeduplicationEntry = {
773
+ reference: ProxyBodyCaptureReference;
774
+ expiresAt: number;
775
+ acknowledged: Promise<boolean>;
776
+ };
729
777
  /** One bounded body publication, tracked across exporter callbacks. */
730
778
  export type ProxyBodyPublicationProgress = {
731
779
  acknowledged: number;
@@ -828,6 +876,7 @@ export type ProxyRequestLoggerSnapshot = {
828
876
  diskEnabled?: boolean;
829
877
  otel?: ReturnType<typeof import("../proxy/otelLogSink.js").getProxyOtelLogSnapshot>;
830
878
  bodyCapture?: ProxyBodyCaptureWorkerSnapshot;
879
+ bodyCapturePolicy?: ProxyBodyCapturePolicySnapshot;
831
880
  enabled: boolean;
832
881
  requests: ProxyRequestLogSinkSnapshot;
833
882
  attempts: ProxyRequestLogSinkSnapshot;
@@ -2563,6 +2612,20 @@ export type UpdateCheckResult = {
2563
2612
  currentVersion: string;
2564
2613
  latestVersion: string;
2565
2614
  updateAvailable: boolean;
2615
+ } & ({
2616
+ checkSucceeded: true;
2617
+ checkError?: never;
2618
+ } | {
2619
+ checkSucceeded: false;
2620
+ checkError: string;
2621
+ updateAvailable: false;
2622
+ });
2623
+ /** Immutable identity of the worker an update attempt is allowed to replace. */
2624
+ export type ProxyServingWorkerIdentity = {
2625
+ version: string;
2626
+ pid: number;
2627
+ /** Rolling generation; null only for a legacy single-process service. */
2628
+ generation: number | null;
2566
2629
  };
2567
2630
  /** Result of one local proxy health probe by the updater or fail-open guard. */
2568
2631
  export type ProxyHealthProbe = {
@@ -2587,6 +2650,9 @@ export type SuppressedVersion = {
2587
2650
  export type UpdateState = {
2588
2651
  lastCheckAt: string;
2589
2652
  lastCheckVersion: string;
2653
+ /** Last attempt is distinct from the last successful registry observation. */
2654
+ lastCheckAttemptAt?: string;
2655
+ lastCheckError?: string | null;
2590
2656
  suppressedVersions: Record<string, SuppressedVersion>;
2591
2657
  /**
2592
2658
  * Last package version whose stable trampoline was successfully validated.
@@ -2621,7 +2687,7 @@ export type UpdateState = {
2621
2687
  export type ProxyUpdateWindowResult = {
2622
2688
  ready: boolean;
2623
2689
  draining: boolean;
2624
- reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout";
2690
+ reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout" | "busy";
2625
2691
  };
2626
2692
  /** Dependencies and timing controls for the updater's safe-window coordinator. */
2627
2693
  export type ProxyUpdateWindowOptions = {
@@ -2629,6 +2695,8 @@ export type ProxyUpdateWindowOptions = {
2629
2695
  quietWaitMs: number;
2630
2696
  drainWaitMs: number;
2631
2697
  pollIntervalMs: number;
2698
+ /** Automatic supervisor refresh must defer instead of draining busy traffic. */
2699
+ allowBusyDrain?: boolean;
2632
2700
  getActivity: () => Promise<ProxyRuntimeActivity | null>;
2633
2701
  setDraining: (draining: boolean) => Promise<boolean>;
2634
2702
  isStopping: () => boolean;
@@ -2705,6 +2773,10 @@ export type ProxyWorkerControlMessage = {
2705
2773
  } | {
2706
2774
  type: "proxy-worker:shutdown";
2707
2775
  generation: number;
2776
+ } | {
2777
+ type: "proxy-worker:socket-offer";
2778
+ generation: number;
2779
+ socketId: string;
2708
2780
  } | {
2709
2781
  type: "proxy-worker:socket-commit";
2710
2782
  generation: number;
@@ -2720,6 +2792,10 @@ export type ProxyWorkerStatusMessage = {
2720
2792
  pid: number;
2721
2793
  version: string;
2722
2794
  processInstanceId?: string;
2795
+ /** New supervisors defer sending descriptors until commit. */
2796
+ socketOfferProtocol?: "control-before-handle";
2797
+ /** New workers confirm descriptor adoption before a generation drains. */
2798
+ socketCommitProtocol?: "worker-ack";
2723
2799
  } | {
2724
2800
  type: "proxy-worker:drained";
2725
2801
  generation: number;
@@ -2738,6 +2814,11 @@ export type ProxyWorkerStatusMessage = {
2738
2814
  generation: number;
2739
2815
  pid: number;
2740
2816
  socketId: string;
2817
+ } | {
2818
+ type: "proxy-worker:socket-committed";
2819
+ generation: number;
2820
+ pid: number;
2821
+ socketId: string;
2741
2822
  } | {
2742
2823
  type: "proxy-worker:replacement-requested";
2743
2824
  generation: number;
@@ -2755,8 +2836,12 @@ export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy"
2755
2836
  export type DetachableTransferableProxySocket = TransferableProxySocket & Pick<import("node:net").Socket, "off">;
2756
2837
  export type RollingWorkerHandle = {
2757
2838
  pid: number;
2839
+ /** Combined offer and commit budget, before any retry. Defaults to 60s. */
2840
+ socketTransferTimeoutMs?: number;
2758
2841
  sendControl: (message: ProxyWorkerControlMessage) => void;
2759
- sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void) => void;
2842
+ sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void,
2843
+ /** Absolute admission deadline shared by queueing and any safe retry. */
2844
+ deadlineAt?: number) => void;
2760
2845
  terminate: (signal?: NodeJS.Signals) => void;
2761
2846
  onMessage: (listener: (message: ProxyWorkerStatusMessage) => void) => () => void;
2762
2847
  onExit: (listener: (code: number | null, signal: string | null) => void) => () => void;
@@ -2853,6 +2938,9 @@ export type RollingProxyServerOptions = {
2853
2938
  shutdownTimeoutMs?: number;
2854
2939
  recoveryDelayMs?: number;
2855
2940
  maxRecoveryDelayMs?: number;
2941
+ /** Backoff after failed pressure-induced replacement, while old worker serves. */
2942
+ stallReplacementDelayMs?: number;
2943
+ maxStallReplacementDelayMs?: number;
2856
2944
  onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
2857
2945
  onEvent?: (event: RollingWorkerSupervisorEvent) => void;
2858
2946
  log?: (message: string) => void;
@@ -2879,8 +2967,12 @@ export type RollingCandidateWorker = RollingManagedWorker & {
2879
2967
  activationRequested: boolean;
2880
2968
  settle: (error?: Error) => void;
2881
2969
  };
2882
- export type RollingQueuedSocket = {
2970
+ export type RollingSocketAdmission = {
2883
2971
  socket: TransferableProxySocket;
2972
+ deadlineAt: number;
2973
+ retried: boolean;
2974
+ };
2975
+ export type RollingQueuedSocket = RollingSocketAdmission & {
2884
2976
  timeout: NodeJS.Timeout;
2885
2977
  };
2886
2978
  export type SocketWorkerRuntime = {
@@ -2896,6 +2988,8 @@ export type SocketWorkerRuntime = {
2896
2988
  };
2897
2989
  export type SocketWorkerRuntimeOptions = {
2898
2990
  onDrained?: () => void;
2991
+ /** Bound for a committed socket's first HTTP request while draining. */
2992
+ firstRequestGraceMs?: number;
2899
2993
  };
2900
2994
  /** Shape of the dynamically-imported js-yaml module. `dump` is optional —
2901
2995
  * read-only consumers (proxy config loader) only need `load`; writers