@juspay/neurolink 9.93.1 → 9.94.0

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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +306 -306
  3. package/dist/cli/commands/proxy.js +687 -200
  4. package/dist/cli/utils/serverUtils.js +4 -1
  5. package/dist/lib/proxy/globalInstaller.js +1 -1
  6. package/dist/lib/proxy/openaiFormat.js +1 -0
  7. package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
  8. package/dist/lib/proxy/rollingProxyServer.js +149 -0
  9. package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
  10. package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
  11. package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
  12. package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
  14. package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
  15. package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
  16. package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
  17. package/dist/lib/proxy/sseInterceptor.js +1 -1
  18. package/dist/lib/proxy/workerLog.d.ts +6 -0
  19. package/dist/lib/proxy/workerLog.js +42 -0
  20. package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
  21. package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
  22. package/dist/lib/types/cli.d.ts +38 -0
  23. package/dist/lib/types/proxy.d.ts +156 -0
  24. package/dist/lib/utils/errorHandling.d.ts +8 -0
  25. package/dist/lib/utils/errorHandling.js +20 -0
  26. package/dist/proxy/globalInstaller.js +1 -1
  27. package/dist/proxy/openaiFormat.js +1 -0
  28. package/dist/proxy/rollingProxyServer.d.ts +3 -0
  29. package/dist/proxy/rollingProxyServer.js +148 -0
  30. package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
  31. package/dist/proxy/rollingWorkerProcess.js +193 -0
  32. package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
  33. package/dist/proxy/rollingWorkerProtocol.js +42 -0
  34. package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
  35. package/dist/proxy/rollingWorkerSupervisor.js +401 -0
  36. package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
  37. package/dist/proxy/socketWorkerRuntime.js +293 -0
  38. package/dist/proxy/sseInterceptor.js +1 -1
  39. package/dist/proxy/workerLog.d.ts +6 -0
  40. package/dist/proxy/workerLog.js +41 -0
  41. package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
  42. package/dist/server/routes/openaiProxyRoutes.js +16 -1
  43. package/dist/types/cli.d.ts +38 -0
  44. package/dist/types/proxy.d.ts +156 -0
  45. package/dist/utils/errorHandling.d.ts +8 -0
  46. package/dist/utils/errorHandling.js +20 -0
  47. package/package.json +3 -2
@@ -0,0 +1,294 @@
1
+ import { isProxyWorkerControlMessage } from "./rollingWorkerProtocol.js";
2
+ function isTransferableProxySocket(handle) {
3
+ if (!handle || typeof handle !== "object") {
4
+ return false;
5
+ }
6
+ const candidate = handle;
7
+ return (typeof candidate.pause === "function" &&
8
+ typeof candidate.resume === "function" &&
9
+ typeof candidate.destroy === "function" &&
10
+ typeof candidate.end === "function" &&
11
+ typeof candidate.once === "function");
12
+ }
13
+ function destroyTransferredHandle(handle) {
14
+ if (handle &&
15
+ typeof handle === "object" &&
16
+ typeof handle.destroy === "function") {
17
+ try {
18
+ handle.destroy();
19
+ }
20
+ catch {
21
+ // Invalid IPC handles must not crash the serving worker.
22
+ }
23
+ }
24
+ }
25
+ /**
26
+ * Adapts transferred TCP sockets to an HTTP server without opening another
27
+ * listener. Active responses finish normally; idle keep-alive sockets close as
28
+ * soon as draining begins.
29
+ */
30
+ export function createSocketWorkerRuntime(server, options) {
31
+ const sockets = new Set();
32
+ const activeBySocket = new Map();
33
+ const activeResponses = new Set();
34
+ let draining = false;
35
+ let drained = false;
36
+ const maybeFinishDrain = () => {
37
+ if (draining && !drained && sockets.size === 0) {
38
+ drained = true;
39
+ options?.onDrained?.();
40
+ }
41
+ };
42
+ const requestStarted = (request, response) => {
43
+ const socket = request.socket;
44
+ activeBySocket.set(socket, (activeBySocket.get(socket) ?? 0) + 1);
45
+ activeResponses.add(response);
46
+ if (draining) {
47
+ response.shouldKeepAlive = false;
48
+ }
49
+ let settled = false;
50
+ const settle = () => {
51
+ if (settled) {
52
+ return;
53
+ }
54
+ settled = true;
55
+ activeResponses.delete(response);
56
+ const remaining = Math.max(0, (activeBySocket.get(socket) ?? 1) - 1);
57
+ if (remaining === 0) {
58
+ activeBySocket.delete(socket);
59
+ if (draining) {
60
+ socket.end();
61
+ }
62
+ }
63
+ else {
64
+ activeBySocket.set(socket, remaining);
65
+ }
66
+ };
67
+ response.once("finish", settle);
68
+ response.once("close", settle);
69
+ };
70
+ server.prependListener("request", requestStarted);
71
+ const acceptSocket = (socket) => {
72
+ if (draining) {
73
+ socket.destroy();
74
+ return;
75
+ }
76
+ sockets.add(socket);
77
+ socket.once("close", () => {
78
+ sockets.delete(socket);
79
+ activeBySocket.delete(socket);
80
+ maybeFinishDrain();
81
+ });
82
+ server.emit("connection", socket);
83
+ socket.resume();
84
+ };
85
+ const drain = () => {
86
+ if (draining) {
87
+ return;
88
+ }
89
+ draining = true;
90
+ for (const response of activeResponses) {
91
+ response.shouldKeepAlive = false;
92
+ }
93
+ for (const socket of sockets) {
94
+ if (!activeBySocket.has(socket)) {
95
+ socket.end();
96
+ }
97
+ }
98
+ maybeFinishDrain();
99
+ };
100
+ return {
101
+ acceptSocket,
102
+ drain,
103
+ close: () => {
104
+ server.off("request", requestStarted);
105
+ for (const socket of sockets) {
106
+ socket.destroy();
107
+ }
108
+ sockets.clear();
109
+ activeBySocket.clear();
110
+ activeResponses.clear();
111
+ draining = true;
112
+ maybeFinishDrain();
113
+ },
114
+ snapshot: () => ({
115
+ draining,
116
+ sockets: sockets.size,
117
+ activeRequests: [...activeBySocket.values()].reduce((total, count) => total + count, 0),
118
+ drained,
119
+ }),
120
+ };
121
+ }
122
+ export function attachSocketWorkerProcess(server, input) {
123
+ let activated = false;
124
+ let gracefulDrain = false;
125
+ const pendingSockets = new Map();
126
+ const send = (message) => {
127
+ if (!process.connected || !process.send) {
128
+ return;
129
+ }
130
+ try {
131
+ process.send(message);
132
+ }
133
+ catch {
134
+ // Parent supervision handles the disconnected worker.
135
+ }
136
+ };
137
+ const runtime = createSocketWorkerRuntime(server, {
138
+ onDrained: () => {
139
+ send({
140
+ type: "proxy-worker:drained",
141
+ generation: input.generation,
142
+ pid: process.pid,
143
+ });
144
+ input.onDrained?.();
145
+ },
146
+ });
147
+ const drainWhenPendingSettled = () => {
148
+ if (!gracefulDrain || pendingSockets.size > 0) {
149
+ return;
150
+ }
151
+ // Defer to a macrotask so a request buffered on a just-committed socket can
152
+ // reach the HTTP server before drain() ends idle sockets; draining
153
+ // synchronously here would truncate that in-flight request.
154
+ setImmediate(() => runtime.drain());
155
+ };
156
+ const onMessage = (message, handle) => {
157
+ if (message &&
158
+ typeof message === "object" &&
159
+ message.type === "proxy-worker:socket") {
160
+ const socketMessage = message;
161
+ if (socketMessage.generation === input.generation &&
162
+ typeof message.socketId === "string" &&
163
+ message.socketId.length > 0 &&
164
+ isTransferableProxySocket(handle)) {
165
+ const socketId = message.socketId;
166
+ const socket = handle;
167
+ socket.pause();
168
+ if (!activated ||
169
+ !process.connected ||
170
+ !process.send ||
171
+ gracefulDrain) {
172
+ socket.destroy();
173
+ return;
174
+ }
175
+ if (pendingSockets.has(socketId)) {
176
+ socket.destroy();
177
+ return;
178
+ }
179
+ pendingSockets.set(socketId, socket);
180
+ try {
181
+ process.send({
182
+ type: "proxy-worker:socket-accepted",
183
+ generation: input.generation,
184
+ pid: process.pid,
185
+ socketId,
186
+ }, (error) => {
187
+ if (error) {
188
+ pendingSockets.delete(socketId);
189
+ socket.destroy();
190
+ }
191
+ });
192
+ }
193
+ catch {
194
+ pendingSockets.delete(socketId);
195
+ socket.destroy();
196
+ }
197
+ }
198
+ else {
199
+ destroyTransferredHandle(handle);
200
+ }
201
+ return;
202
+ }
203
+ if (handle !== undefined) {
204
+ destroyTransferredHandle(handle);
205
+ }
206
+ if (isProxyWorkerControlMessage(message) &&
207
+ message.generation === input.generation) {
208
+ if (message.type === "proxy-worker:activate") {
209
+ if (!activated) {
210
+ try {
211
+ input.onActivated?.();
212
+ activated = true;
213
+ send({
214
+ type: "proxy-worker:activated",
215
+ generation: input.generation,
216
+ pid: process.pid,
217
+ });
218
+ }
219
+ catch (error) {
220
+ send({
221
+ type: "proxy-worker:fatal",
222
+ generation: input.generation,
223
+ pid: process.pid,
224
+ message: error instanceof Error ? error.message : String(error),
225
+ });
226
+ runtime.drain();
227
+ }
228
+ }
229
+ }
230
+ else if (message.type === "proxy-worker:socket-commit" ||
231
+ message.type === "proxy-worker:socket-cancel") {
232
+ const socket = pendingSockets.get(message.socketId);
233
+ if (!socket) {
234
+ return;
235
+ }
236
+ pendingSockets.delete(message.socketId);
237
+ if (message.type === "proxy-worker:socket-commit") {
238
+ runtime.acceptSocket(socket);
239
+ }
240
+ else {
241
+ socket.destroy();
242
+ }
243
+ drainWhenPendingSettled();
244
+ }
245
+ else {
246
+ // Graceful drain: a socket-commit for an already-accepted socket may
247
+ // still be in flight. Stop admitting new offers and defer the drain
248
+ // until every pending socket is committed or canceled, so an
249
+ // acknowledged connection is served instead of silently dropped.
250
+ gracefulDrain = true;
251
+ drainWhenPendingSettled();
252
+ }
253
+ }
254
+ };
255
+ // Terminal shutdown path: the parent IPC channel is gone (disconnect) or the
256
+ // process is being killed (SIGTERM/SIGINT). Unlike the graceful rolling drain
257
+ // above, pending offers can no longer be committed, so they are destroyed and
258
+ // the drain runs immediately — a best-effort close is correct when the process
259
+ // is exiting. The zero-downtime guarantee applies to the rolling handoff
260
+ // (control-message) path, not to process termination.
261
+ const drain = () => {
262
+ for (const socket of pendingSockets.values()) {
263
+ socket.destroy();
264
+ }
265
+ pendingSockets.clear();
266
+ runtime.drain();
267
+ };
268
+ const onTerminationSignal = () => drain();
269
+ process.on("message", onMessage);
270
+ process.once("disconnect", drain);
271
+ process.once("SIGTERM", onTerminationSignal);
272
+ process.once("SIGINT", onTerminationSignal);
273
+ send({
274
+ type: "proxy-worker:ready",
275
+ generation: input.generation,
276
+ pid: process.pid,
277
+ version: input.version,
278
+ });
279
+ return {
280
+ ...runtime,
281
+ close: () => {
282
+ process.off("message", onMessage);
283
+ process.off("disconnect", drain);
284
+ process.off("SIGTERM", onTerminationSignal);
285
+ process.off("SIGINT", onTerminationSignal);
286
+ for (const socket of pendingSockets.values()) {
287
+ socket.destroy();
288
+ }
289
+ pendingSockets.clear();
290
+ runtime.close();
291
+ },
292
+ };
293
+ }
294
+ //# sourceMappingURL=socketWorkerRuntime.js.map
@@ -337,7 +337,7 @@ function processEvent(acc, event) {
337
337
  const message = nestedError?.message ?? payload.message;
338
338
  acc.streamErrorMessage =
339
339
  typeof message === "string" && message.trim()
340
- ? message.trim()
340
+ ? truncateString(message.trim(), MAX_EVENT_DATA_BYTES)
341
341
  : truncateString(event.data, MAX_EVENT_DATA_BYTES);
342
342
  }
343
343
  switch (event.event) {
@@ -0,0 +1,6 @@
1
+ /** Open a restrictive worker log without making proxy startup depend on it. */
2
+ export declare function openProxyWorkerLog(filename: string, logDir?: string): {
3
+ stdio: number | "ignore";
4
+ close: () => void;
5
+ error?: string;
6
+ };
@@ -0,0 +1,42 @@
1
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, statSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ /** Open a restrictive worker log without making proxy startup depend on it. */
5
+ export function openProxyWorkerLog(filename, logDir = join(homedir(), ".neurolink", "logs")) {
6
+ let fd;
7
+ try {
8
+ if (basename(filename) !== filename) {
9
+ throw new Error("worker log filename must not contain a path");
10
+ }
11
+ if (!existsSync(logDir)) {
12
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
13
+ }
14
+ else if (!statSync(logDir).isDirectory()) {
15
+ throw new Error("worker log path exists but is not a directory");
16
+ }
17
+ chmodSync(logDir, 0o700);
18
+ const path = join(logDir, filename);
19
+ fd = openSync(path, "a", 0o600);
20
+ chmodSync(path, 0o600);
21
+ return {
22
+ stdio: fd,
23
+ close: () => closeSync(fd),
24
+ };
25
+ }
26
+ catch (error) {
27
+ if (fd !== undefined) {
28
+ try {
29
+ closeSync(fd);
30
+ }
31
+ catch {
32
+ // The log is optional and startup must remain fail-open.
33
+ }
34
+ }
35
+ return {
36
+ stdio: "ignore",
37
+ close: () => undefined,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ };
40
+ }
41
+ }
42
+ //# sourceMappingURL=workerLog.js.map
@@ -12,6 +12,11 @@
12
12
  * provider/model pairs (e.g. "gpt-4o" -> vertex/gemini-2.5-pro).
13
13
  */
14
14
  import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } from "../../types/index.js";
15
+ declare function resolveStreamCancellationLifecycle(terminalStreamError: string | undefined): {
16
+ status: number;
17
+ errorType: string;
18
+ errorMessage: string;
19
+ };
15
20
  /**
16
21
  * Create OpenAI-compatible proxy routes.
17
22
  *
@@ -27,3 +32,7 @@ import type { ModelRouterInterface, ProxyRuntimeConfigProvider, RouteGroup } fro
27
32
  * @returns RouteGroup with OpenAI-compatible endpoints.
28
33
  */
29
34
  export declare function createOpenAIProxyRoutes(modelRouter?: ModelRouterInterface, basePath?: string, loopbackPort?: number, runtimeConfigProvider?: ProxyRuntimeConfigProvider): RouteGroup;
35
+ export declare const __testHooks: {
36
+ resolveStreamCancellationLifecycle: typeof resolveStreamCancellationLifecycle;
37
+ };
38
+ export {};
@@ -27,6 +27,19 @@ const LOOPBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — long enough for slow
27
27
  // `createOpenAIProxyRoutes`'s third argument when the actual listener port is
28
28
  // known (e.g. when started from the CLI handler).
29
29
  const DEFAULT_LOOPBACK_PORT = 55669;
30
+ function resolveStreamCancellationLifecycle(terminalStreamError) {
31
+ return terminalStreamError
32
+ ? {
33
+ status: 502,
34
+ errorType: "loopback_stream_error",
35
+ errorMessage: terminalStreamError,
36
+ }
37
+ : {
38
+ status: 499,
39
+ errorType: "client_cancelled",
40
+ errorMessage: "Client cancelled the OpenAI-compatible stream",
41
+ };
42
+ }
30
43
  /**
31
44
  * Build an OpenAI-shaped error as a typed Response with the intended status.
32
45
  *
@@ -188,7 +201,8 @@ async function handleOpenAIToAnthropicBridge(args) {
188
201
  await reader.cancel(reason);
189
202
  }
190
203
  finally {
191
- await finishLifecycle(499, "client_cancelled", "Client cancelled the OpenAI-compatible stream");
204
+ const cancellation = resolveStreamCancellationLifecycle(terminalStreamError);
205
+ await finishLifecycle(cancellation.status, cancellation.errorType, cancellation.errorMessage);
192
206
  }
193
207
  },
194
208
  });
@@ -381,4 +395,5 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
381
395
  ],
382
396
  };
383
397
  }
398
+ export const __testHooks = { resolveStreamCancellationLifecycle };
384
399
  //# sourceMappingURL=openaiProxyRoutes.js.map
@@ -820,6 +820,42 @@ export type FallbackInfo = {
820
820
  provider: string;
821
821
  model: string;
822
822
  };
823
+ export type ProxyRollingState = {
824
+ generation: number;
825
+ active: {
826
+ pid: number;
827
+ version: string;
828
+ generation: number;
829
+ } | null;
830
+ candidate: {
831
+ pid: number;
832
+ expectedVersion: string;
833
+ generation: number;
834
+ } | null;
835
+ draining: Array<{
836
+ pid: number;
837
+ version: string;
838
+ generation: number;
839
+ }>;
840
+ queuedSockets: number;
841
+ rejectedSockets: number;
842
+ failedTransfers: number;
843
+ lastFailure: {
844
+ at: string;
845
+ generation: number;
846
+ version: string;
847
+ phase: "startup" | "activation" | "runtime" | "transfer";
848
+ message: string;
849
+ } | null;
850
+ };
851
+ export type ProxySupervisorState = {
852
+ pid: number;
853
+ host: string;
854
+ port: number;
855
+ startTime: string;
856
+ updaterPid?: number;
857
+ rolling: ProxyRollingState;
858
+ };
823
859
  /** Persisted state for a running proxy instance */
824
860
  export type ProxyState = {
825
861
  pid: number;
@@ -840,6 +876,8 @@ export type ProxyState = {
840
876
  guardPid?: number;
841
877
  /** Dedicated updater PID for launchd-managed proxy installations. */
842
878
  updaterPid?: number;
879
+ /** Stable listener supervisor PID when requests are served by socket workers. */
880
+ supervisorPid?: number;
843
881
  /** How the proxy was launched — "launchd" if installed as service, "manual" otherwise */
844
882
  managedBy?: "launchd" | "manual";
845
883
  /** Whether the proxy is running in transparent passthrough mode */
@@ -1455,6 +1455,162 @@ export type UpdaterWorkerSupervisor = {
1455
1455
  checkNow: () => number | undefined;
1456
1456
  stop: () => void;
1457
1457
  };
1458
+ export type ProxyWorkerControlMessage = {
1459
+ type: "proxy-worker:activate";
1460
+ generation: number;
1461
+ } | {
1462
+ type: "proxy-worker:drain";
1463
+ generation: number;
1464
+ } | {
1465
+ type: "proxy-worker:shutdown";
1466
+ generation: number;
1467
+ } | {
1468
+ type: "proxy-worker:socket-commit";
1469
+ generation: number;
1470
+ socketId: string;
1471
+ } | {
1472
+ type: "proxy-worker:socket-cancel";
1473
+ generation: number;
1474
+ socketId: string;
1475
+ };
1476
+ export type ProxyWorkerStatusMessage = {
1477
+ type: "proxy-worker:ready";
1478
+ generation: number;
1479
+ pid: number;
1480
+ version: string;
1481
+ } | {
1482
+ type: "proxy-worker:drained";
1483
+ generation: number;
1484
+ pid: number;
1485
+ } | {
1486
+ type: "proxy-worker:activated";
1487
+ generation: number;
1488
+ pid: number;
1489
+ } | {
1490
+ type: "proxy-worker:fatal";
1491
+ generation: number;
1492
+ pid: number;
1493
+ message: string;
1494
+ } | {
1495
+ type: "proxy-worker:socket-accepted";
1496
+ generation: number;
1497
+ pid: number;
1498
+ socketId: string;
1499
+ };
1500
+ export type ProxyWorkerSocketMessage = {
1501
+ type: "proxy-worker:socket";
1502
+ generation: number;
1503
+ socketId: string;
1504
+ };
1505
+ export type ProxyWorkerIpcMessage = ProxyWorkerControlMessage | ProxyWorkerStatusMessage | ProxyWorkerSocketMessage;
1506
+ export type TransferableProxySocket = Pick<import("node:net").Socket, "destroy" | "end" | "pause" | "resume" | "once">;
1507
+ export type RollingWorkerHandle = {
1508
+ pid: number;
1509
+ sendControl: (message: ProxyWorkerControlMessage) => void;
1510
+ sendSocket: (generation: number, socket: TransferableProxySocket, callback: (error?: Error | null) => void) => void;
1511
+ terminate: (signal?: NodeJS.Signals) => void;
1512
+ onMessage: (listener: (message: ProxyWorkerStatusMessage) => void) => () => void;
1513
+ onExit: (listener: (code: number | null, signal: string | null) => void) => () => void;
1514
+ };
1515
+ export type SpawnProxySocketWorkerOptions = {
1516
+ generation: number;
1517
+ expectedVersion: string;
1518
+ command: string;
1519
+ args: string[];
1520
+ env?: NodeJS.ProcessEnv;
1521
+ stdout?: "inherit" | "ignore";
1522
+ stderr?: "inherit" | "ignore";
1523
+ socketAckTimeoutMs?: number;
1524
+ };
1525
+ export type RollingWorkerSupervisorSnapshot = {
1526
+ generation: number;
1527
+ active: {
1528
+ pid: number;
1529
+ version: string;
1530
+ generation: number;
1531
+ } | null;
1532
+ candidate: {
1533
+ pid: number;
1534
+ expectedVersion: string;
1535
+ generation: number;
1536
+ } | null;
1537
+ draining: Array<{
1538
+ pid: number;
1539
+ version: string;
1540
+ generation: number;
1541
+ }>;
1542
+ queuedSockets: number;
1543
+ rejectedSockets: number;
1544
+ failedTransfers: number;
1545
+ lastFailure: {
1546
+ at: string;
1547
+ generation: number;
1548
+ version: string;
1549
+ phase: "startup" | "activation" | "runtime" | "transfer";
1550
+ message: string;
1551
+ } | null;
1552
+ };
1553
+ export type RollingWorkerSupervisorOptions = {
1554
+ spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
1555
+ readyTimeoutMs?: number;
1556
+ socketQueueLimit?: number;
1557
+ socketQueueTimeoutMs?: number;
1558
+ shutdownTimeoutMs?: number;
1559
+ onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
1560
+ log?: (message: string) => void;
1561
+ };
1562
+ export type RollingProxyServerOptions = {
1563
+ host: string;
1564
+ port: number;
1565
+ initialVersion: string;
1566
+ spawnWorker: (generation: number, expectedVersion: string) => RollingWorkerHandle;
1567
+ readyTimeoutMs?: number;
1568
+ socketQueueLimit?: number;
1569
+ socketQueueTimeoutMs?: number;
1570
+ shutdownTimeoutMs?: number;
1571
+ recoveryDelayMs?: number;
1572
+ maxRecoveryDelayMs?: number;
1573
+ onStateChange?: (snapshot: RollingWorkerSupervisorSnapshot) => void;
1574
+ log?: (message: string) => void;
1575
+ };
1576
+ export type RollingProxyServer = {
1577
+ address: {
1578
+ host: string;
1579
+ port: number;
1580
+ };
1581
+ replace: (expectedVersion: string) => Promise<RollingWorkerSupervisorSnapshot>;
1582
+ snapshot: () => RollingWorkerSupervisorSnapshot;
1583
+ close: () => Promise<void>;
1584
+ };
1585
+ export type RollingManagedWorker = {
1586
+ handle: RollingWorkerHandle;
1587
+ generation: number;
1588
+ version: string;
1589
+ dispose: () => void;
1590
+ };
1591
+ export type RollingCandidateWorker = RollingManagedWorker & {
1592
+ expectedVersion: string;
1593
+ activationRequested: boolean;
1594
+ settle: (error?: Error) => void;
1595
+ };
1596
+ export type RollingQueuedSocket = {
1597
+ socket: TransferableProxySocket;
1598
+ timeout: NodeJS.Timeout;
1599
+ };
1600
+ export type SocketWorkerRuntime = {
1601
+ acceptSocket: (socket: TransferableProxySocket) => void;
1602
+ drain: () => void;
1603
+ close: () => void;
1604
+ snapshot: () => {
1605
+ draining: boolean;
1606
+ sockets: number;
1607
+ activeRequests: number;
1608
+ drained: boolean;
1609
+ };
1610
+ };
1611
+ export type SocketWorkerRuntimeOptions = {
1612
+ onDrained?: () => void;
1613
+ };
1458
1614
  /** Shape of the dynamically-imported js-yaml module. `dump` is optional —
1459
1615
  * read-only consumers (proxy config loader) only need `load`; writers
1460
1616
  * (CLI primary-account commands) check `dump` before calling. */
@@ -14,6 +14,7 @@ export declare const ERROR_CODES: {
14
14
  readonly MEMORY_EXHAUSTED: "MEMORY_EXHAUSTED";
15
15
  readonly NETWORK_ERROR: "NETWORK_ERROR";
16
16
  readonly PERMISSION_DENIED: "PERMISSION_DENIED";
17
+ readonly PROXY_WORKER_LIFECYCLE_FAILED: "PROXY_WORKER_LIFECYCLE_FAILED";
17
18
  readonly PROVIDER_NOT_AVAILABLE: "PROVIDER_NOT_AVAILABLE";
18
19
  readonly PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED";
19
20
  readonly PROVIDER_QUOTA_EXCEEDED: "PROVIDER_QUOTA_EXCEEDED";
@@ -247,6 +248,13 @@ export declare class ErrorFactory {
247
248
  * Create an evaluation execution failed error
248
249
  */
249
250
  static evaluationExecutionFailed(operation: string, originalError: Error): NeuroLinkError;
251
+ /**
252
+ * Create a proxy rolling-worker lifecycle error. Preserves the caller's exact
253
+ * message so existing substring-based assertions keep working, while giving
254
+ * callers a typed `code`/`category` to branch on instead of string-matching
255
+ * generic `Error` instances.
256
+ */
257
+ static proxyWorkerLifecycle(message: string, context?: Record<string, unknown>): NeuroLinkError;
250
258
  }
251
259
  /**
252
260
  * Timeout wrapper for async operations
@@ -20,6 +20,7 @@ export const ERROR_CODES = {
20
20
  MEMORY_EXHAUSTED: "MEMORY_EXHAUSTED",
21
21
  NETWORK_ERROR: "NETWORK_ERROR",
22
22
  PERMISSION_DENIED: "PERMISSION_DENIED",
23
+ PROXY_WORKER_LIFECYCLE_FAILED: "PROXY_WORKER_LIFECYCLE_FAILED",
23
24
  // Provider errors
24
25
  PROVIDER_NOT_AVAILABLE: "PROVIDER_NOT_AVAILABLE",
25
26
  PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
@@ -818,6 +819,25 @@ export class ErrorFactory {
818
819
  originalError,
819
820
  });
820
821
  }
822
+ // ============================================================================
823
+ // PROXY ROLLING-WORKER ERRORS
824
+ // ============================================================================
825
+ /**
826
+ * Create a proxy rolling-worker lifecycle error. Preserves the caller's exact
827
+ * message so existing substring-based assertions keep working, while giving
828
+ * callers a typed `code`/`category` to branch on instead of string-matching
829
+ * generic `Error` instances.
830
+ */
831
+ static proxyWorkerLifecycle(message, context) {
832
+ return new NeuroLinkError({
833
+ code: ERROR_CODES.PROXY_WORKER_LIFECYCLE_FAILED,
834
+ message,
835
+ category: ErrorCategory.SYSTEM,
836
+ severity: ErrorSeverity.HIGH,
837
+ retriable: false,
838
+ context: context || {},
839
+ });
840
+ }
821
841
  }
822
842
  /**
823
843
  * Timeout wrapper for async operations
@@ -14,7 +14,7 @@ function writableDirectory(path) {
14
14
  if (!existsSync(path)) {
15
15
  return false;
16
16
  }
17
- accessSync(path, constants.W_OK);
17
+ accessSync(path, constants.W_OK | constants.X_OK);
18
18
  return true;
19
19
  }
20
20
  catch {