@juspay/neurolink 9.93.2 → 9.94.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 (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/browser/neurolink.min.js +263 -263
  3. package/dist/cli/commands/proxy.js +672 -182
  4. package/dist/cli/utils/audioPlayer.d.ts +44 -9
  5. package/dist/cli/utils/audioPlayer.js +162 -65
  6. package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
  7. package/dist/lib/proxy/rollingProxyServer.js +149 -0
  8. package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
  9. package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
  10. package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
  11. package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
  12. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
  14. package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
  15. package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
  16. package/dist/lib/skills/skillMatcher.d.ts +7 -1
  17. package/dist/lib/skills/skillMatcher.js +23 -8
  18. package/dist/lib/types/cli.d.ts +47 -0
  19. package/dist/lib/types/proxy.d.ts +156 -0
  20. package/dist/lib/utils/errorHandling.d.ts +8 -0
  21. package/dist/lib/utils/errorHandling.js +20 -0
  22. package/dist/proxy/rollingProxyServer.d.ts +3 -0
  23. package/dist/proxy/rollingProxyServer.js +148 -0
  24. package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
  25. package/dist/proxy/rollingWorkerProcess.js +193 -0
  26. package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
  27. package/dist/proxy/rollingWorkerProtocol.js +42 -0
  28. package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
  29. package/dist/proxy/rollingWorkerSupervisor.js +401 -0
  30. package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
  31. package/dist/proxy/socketWorkerRuntime.js +293 -0
  32. package/dist/skills/skillMatcher.d.ts +7 -1
  33. package/dist/skills/skillMatcher.js +23 -8
  34. package/dist/types/cli.d.ts +47 -0
  35. package/dist/types/proxy.d.ts +156 -0
  36. package/dist/utils/errorHandling.d.ts +8 -0
  37. package/dist/utils/errorHandling.js +20 -0
  38. package/package.json +3 -2
@@ -0,0 +1,14 @@
1
+ import type { Server } from "node:http";
2
+ import type { SocketWorkerRuntime, SocketWorkerRuntimeOptions } from "../types/index.js";
3
+ /**
4
+ * Adapts transferred TCP sockets to an HTTP server without opening another
5
+ * listener. Active responses finish normally; idle keep-alive sockets close as
6
+ * soon as draining begins.
7
+ */
8
+ export declare function createSocketWorkerRuntime(server: Server, options?: SocketWorkerRuntimeOptions): SocketWorkerRuntime;
9
+ export declare function attachSocketWorkerProcess(server: Server, input: {
10
+ generation: number;
11
+ version: string;
12
+ onActivated?: () => void;
13
+ onDrained?: () => void;
14
+ }): SocketWorkerRuntime;
@@ -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
@@ -26,7 +26,13 @@ export declare function filterSkillIndex(items: SkillIndexItem[], query: SkillSe
26
26
  * keys are built by concatenation (S3, Redis) or path joining.
27
27
  */
28
28
  export declare function isSafeSkillResourcePath(resourcePath: string): boolean;
29
- /** Whether a skill is visible from the calling scope. */
29
+ /**
30
+ * Whether a skill is visible from the calling scope. Fails closed, matching
31
+ * `filterSkillIndex`: a scoped skill is hidden unless the caller supplies a
32
+ * `scopeId` that the skill explicitly lists. Omitting `scopeId` therefore
33
+ * yields global skills only — it must never expose a scoped skill by name
34
+ * through `use_skill` / `read_skill_resource` (#1139).
35
+ */
30
36
  export declare function isSkillVisibleInScope(skill: Pick<SkillDefinition, "scope" | "scopeIds">, scopeId?: string): boolean;
31
37
  /**
32
38
  * Render the `<available_skills>` block embedded in the use_skill tool
@@ -29,11 +29,14 @@ export function filterSkillIndex(items, query) {
29
29
  if ((item.status ?? "active") !== "active") {
30
30
  return false;
31
31
  }
32
- // Scope filter: global skills always pass; scoped skills require a
33
- // matching scopeId. When the caller provides no scopeId, scoped skills
34
- // still pass (curator semantics unscoped callers see everything).
35
- if (query.scopeId && item.scope === "scoped") {
36
- if (!(item.scopeIds ?? []).includes(query.scopeId)) {
32
+ // Scope filter: global skills always pass. Scoped skills require a
33
+ // scopeId that matches; when the caller supplies no scopeId we fail
34
+ // closed and exclude them, so a shared multi-tenant instance that forgets
35
+ // to pass a per-call scopeId never leaks one tenant's scoped skills to
36
+ // everyone (#1139). Callers that legitimately want a tenant's scoped
37
+ // skills must pass that tenant's scopeId.
38
+ if (item.scope === "scoped") {
39
+ if (!query.scopeId || !(item.scopeIds ?? []).includes(query.scopeId)) {
37
40
  return false;
38
41
  }
39
42
  }
@@ -67,11 +70,20 @@ export function isSafeSkillResourcePath(resourcePath) {
67
70
  !normalized.startsWith("/") &&
68
71
  !normalized.split("/").some((segment) => segment === ".." || segment === ""));
69
72
  }
70
- /** Whether a skill is visible from the calling scope. */
73
+ /**
74
+ * Whether a skill is visible from the calling scope. Fails closed, matching
75
+ * `filterSkillIndex`: a scoped skill is hidden unless the caller supplies a
76
+ * `scopeId` that the skill explicitly lists. Omitting `scopeId` therefore
77
+ * yields global skills only — it must never expose a scoped skill by name
78
+ * through `use_skill` / `read_skill_resource` (#1139).
79
+ */
71
80
  export function isSkillVisibleInScope(skill, scopeId) {
72
- if (!scopeId || skill.scope !== "scoped") {
81
+ if (skill.scope !== "scoped") {
73
82
  return true;
74
83
  }
84
+ if (!scopeId) {
85
+ return false;
86
+ }
75
87
  return (skill.scopeIds ?? []).includes(scopeId);
76
88
  }
77
89
  /** Trim a description to its first sentence (or the whole text when unsplittable). */
@@ -127,7 +139,10 @@ export function renderSkillListing(items, budgetChars) {
127
139
  * Returns null when nothing is visible so callers can skip injection.
128
140
  */
129
141
  export function formatSkillsPromptIndex(items, maxItems) {
130
- if (items.length === 0) {
142
+ // Nothing visible, or the index is explicitly disabled (maxItems <= 0):
143
+ // skip injection entirely rather than emit a header + "N more skills exist"
144
+ // note with zero skill lines (#1139).
145
+ if (items.length === 0 || maxItems <= 0) {
131
146
  return null;
132
147
  }
133
148
  const visible = items.slice(0, maxItems);
@@ -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 */
@@ -1637,3 +1675,12 @@ export type CliNetworkCommandArgs = BaseCommandArgs & {
1637
1675
  /** Show detailed information */
1638
1676
  detailed?: boolean;
1639
1677
  };
1678
+ /**
1679
+ * A single audio-player invocation for CLI TTS playback: a binary plus its
1680
+ * arguments for `execFile`. The player list is tried in order until one
1681
+ * succeeds (see `src/cli/utils/audioPlayer.ts`).
1682
+ */
1683
+ export type CliAudioPlayerCommand = {
1684
+ command: string;
1685
+ args: string[];
1686
+ };
@@ -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
@@ -0,0 +1,3 @@
1
+ import type { RollingProxyServer, RollingProxyServerOptions } from "../types/index.js";
2
+ /** Keep one public listener stable while serving workers start and rotate. */
3
+ export declare function startRollingProxyServer(options: RollingProxyServerOptions): Promise<RollingProxyServer>;