@juspay/neurolink 12.19.2 → 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 (50) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/adapters/imageFormatSupport.d.ts +5 -2
  3. package/dist/adapters/imageFormatSupport.js +47 -24
  4. package/dist/browser/neurolink.min.js +394 -394
  5. package/dist/cli/commands/proxy.js +171 -25
  6. package/dist/cli/commands/telemetry.d.ts +4 -1
  7. package/dist/cli/commands/telemetry.js +113 -0
  8. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  9. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  10. package/dist/cli/utils/nativeTelemetry.js +322 -0
  11. package/dist/core/modules/MessageBuilder.js +2 -0
  12. package/dist/neurolink.js +1 -0
  13. package/dist/processors/config/fileExtensions.d.ts +8 -1
  14. package/dist/processors/config/fileExtensions.js +8 -1
  15. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  16. package/dist/proxy/bodyCapturePolicy.js +132 -0
  17. package/dist/proxy/otelLogSink.d.ts +11 -1
  18. package/dist/proxy/otelLogSink.js +174 -27
  19. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  20. package/dist/proxy/otlpLogTransport.js +22 -7
  21. package/dist/proxy/requestLogger.js +11 -3
  22. package/dist/proxy/restartControl.js +18 -3
  23. package/dist/proxy/rollingProxyServer.js +48 -1
  24. package/dist/proxy/rollingWorkerProcess.js +87 -10
  25. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  26. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  27. package/dist/proxy/socketWorkerRuntime.js +90 -4
  28. package/dist/proxy/updateChecker.d.ts +3 -3
  29. package/dist/proxy/updateChecker.js +16 -9
  30. package/dist/proxy/updateCoordinator.d.ts +10 -1
  31. package/dist/proxy/updateCoordinator.js +68 -1
  32. package/dist/proxy/updateState.d.ts +2 -0
  33. package/dist/proxy/updateState.js +15 -0
  34. package/dist/rag/pipeline/RAGPipeline.js +29 -0
  35. package/dist/rag/ragIntegration.js +21 -1
  36. package/dist/types/cli.d.ts +18 -0
  37. package/dist/types/file.d.ts +10 -0
  38. package/dist/types/generate.d.ts +27 -1
  39. package/dist/types/proxy.d.ts +97 -3
  40. package/dist/types/stream.d.ts +13 -1
  41. package/dist/utils/imageProcessor.js +151 -6
  42. package/dist/utils/messageBuilder.d.ts +2 -2
  43. package/dist/utils/messageBuilder.js +23 -13
  44. package/dist/utils/multimodalOptionsBuilder.d.ts +3 -0
  45. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  46. package/docs-site/static/search-index.json +1 -1
  47. package/package.json +2 -1
  48. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  49. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  50. package/scripts/observability/proxy-telemetry-check.mjs +135 -50
@@ -9,6 +9,8 @@ import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER
9
9
  export function spawnProxySocketWorker(options) {
10
10
  const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 30_000);
11
11
  let nextSocketId = 0;
12
+ let controlBeforeHandle = false;
13
+ let workerCommitAcknowledgement = false;
12
14
  const pendingSockets = new Map();
13
15
  const statusListeners = new Set();
14
16
  const pendingStatusMessages = [];
@@ -124,6 +126,26 @@ export function spawnProxySocketWorker(options) {
124
126
  * the commit phase.
125
127
  */
126
128
  const onInternalMessage = (message) => {
129
+ if (isProxyWorkerStatusMessage(message) &&
130
+ message.type === "proxy-worker:ready" &&
131
+ message.generation === options.generation &&
132
+ message.pid === childPid) {
133
+ controlBeforeHandle =
134
+ message.socketOfferProtocol === "control-before-handle";
135
+ workerCommitAcknowledgement =
136
+ message.socketCommitProtocol === "worker-ack";
137
+ }
138
+ if (isProxyWorkerStatusMessage(message) &&
139
+ message.type === "proxy-worker:socket-committed" &&
140
+ message.generation === options.generation &&
141
+ message.pid === childPid) {
142
+ const pending = pendingSockets.get(message.socketId);
143
+ if (!pending || !pending.accepted || !pending.descriptorSent) {
144
+ return;
145
+ }
146
+ settleSocket(message.socketId);
147
+ return;
148
+ }
127
149
  if (isProxyWorkerStatusMessage(message) &&
128
150
  message.type === "proxy-worker:socket-accepted" &&
129
151
  message.generation === options.generation &&
@@ -132,22 +154,56 @@ export function spawnProxySocketWorker(options) {
132
154
  if (!pending || pending.accepted) {
133
155
  return;
134
156
  }
157
+ if (Date.now() >= pending.deadlineAt) {
158
+ const error = Object.assign(new Error("Socket admission deadline expired before commit"), {
159
+ code: PROXY_SOCKET_OFFER_TIMEOUT,
160
+ socketNeverTransferred: !pending.descriptorSent,
161
+ });
162
+ settleSocket(message.socketId, error);
163
+ return;
164
+ }
165
+ const commitBudgetMs = Math.min(socketAckTimeoutMs, pending.deadlineAt - Date.now());
166
+ const minimumCommitWindowMs = Math.min(socketAckTimeoutMs, 1_000);
167
+ if (commitBudgetMs < minimumCommitWindowMs) {
168
+ settleSocket(message.socketId, Object.assign(new Error("Socket admission deadline too close for commit"), {
169
+ code: PROXY_SOCKET_OFFER_TIMEOUT,
170
+ socketNeverTransferred: !pending.descriptorSent,
171
+ }));
172
+ return;
173
+ }
135
174
  pending.accepted = true;
136
175
  // Acceptance and commit are distinct phases. A late acceptance must not
137
176
  // inherit an almost-expired offer timer and kill established streams.
138
177
  clearTimeout(pending.timeout);
139
178
  pending.timeout = setTimeout(() => {
140
- const error = new Error(`proxy worker ${childPid} socket commit remained pending for ${socketAckTimeoutMs}ms`);
179
+ const error = new Error(`proxy worker ${childPid} socket commit remained pending for ${commitBudgetMs}ms`);
141
180
  error.code = PROXY_SOCKET_COMMIT_TIMEOUT;
142
181
  settleSocket(message.socketId, error);
143
- }, socketAckTimeoutMs);
182
+ }, commitBudgetMs);
144
183
  pending.timeout.unref?.();
145
184
  try {
146
- child.send({
185
+ const commit = {
147
186
  type: "proxy-worker:socket-commit",
148
187
  generation: options.generation,
149
188
  socketId: message.socketId,
150
- }, (error) => settleSocket(message.socketId, error ?? undefined));
189
+ };
190
+ if (!pending.descriptorSent) {
191
+ // Mark ambiguous before calling IPC: even a thrown/send callback error
192
+ // cannot prove the descriptor was not delivered and consumed.
193
+ pending.descriptorSent = true;
194
+ child.send(commit, pending.socket, { keepOpen: true }, (error) => {
195
+ if (error || !workerCommitAcknowledgement) {
196
+ settleSocket(message.socketId, error ?? undefined);
197
+ }
198
+ });
199
+ }
200
+ else {
201
+ child.send(commit, (error) => {
202
+ if (error || !workerCommitAcknowledgement) {
203
+ settleSocket(message.socketId, error ?? undefined);
204
+ }
205
+ });
206
+ }
151
207
  }
152
208
  catch (error) {
153
209
  settleSocket(message.socketId, error instanceof Error ? error : new Error(String(error)));
@@ -208,8 +264,17 @@ export function spawnProxySocketWorker(options) {
208
264
  };
209
265
  return {
210
266
  pid: childPid,
267
+ socketTransferTimeoutMs: 2 * socketAckTimeoutMs,
211
268
  sendControl,
212
- sendSocket: (generation, socket, callback) => {
269
+ sendSocket: (generation, socket, callback, deadlineAt = Date.now() + 2 * socketAckTimeoutMs) => {
270
+ if (Date.now() >= deadlineAt) {
271
+ const error = Object.assign(new Error("Socket admission deadline expired before offer"), {
272
+ code: PROXY_SOCKET_OFFER_TIMEOUT,
273
+ socketNeverTransferred: true,
274
+ });
275
+ callback(error);
276
+ return;
277
+ }
213
278
  if (budgetResponseFailure) {
214
279
  callback(budgetResponseFailure);
215
280
  return;
@@ -220,23 +285,35 @@ export function spawnProxySocketWorker(options) {
220
285
  }
221
286
  const socketId = `${generation}:${++nextSocketId}`;
222
287
  const timeout = setTimeout(() => {
223
- const error = new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`);
224
- error.code = PROXY_SOCKET_OFFER_TIMEOUT;
288
+ const error = Object.assign(new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`), {
289
+ code: PROXY_SOCKET_OFFER_TIMEOUT,
290
+ socketNeverTransferred: pendingSockets.get(socketId)?.descriptorSent === false,
291
+ });
225
292
  settleSocket(socketId, error);
226
- }, socketAckTimeoutMs);
293
+ }, Math.max(0, Math.min(socketAckTimeoutMs, deadlineAt - Date.now())));
227
294
  timeout.unref?.();
228
295
  pendingSockets.set(socketId, {
229
296
  socket,
230
297
  callback,
231
298
  timeout,
232
299
  accepted: false,
300
+ deadlineAt,
301
+ descriptorSent: !controlBeforeHandle,
233
302
  });
234
303
  try {
235
- child.send({ type: "proxy-worker:socket", generation, socketId }, socket, { keepOpen: true }, (error) => {
304
+ const onSent = (error) => {
236
305
  if (error) {
237
306
  settleSocket(socketId, error);
238
307
  }
239
- });
308
+ };
309
+ if (controlBeforeHandle) {
310
+ child.send({ type: "proxy-worker:socket-offer", generation, socketId }, onSent);
311
+ }
312
+ else {
313
+ // Old workers require descriptor-first offers. They may buffer input
314
+ // before commit, so their timed-out offers are never safe to retry.
315
+ child.send({ type: "proxy-worker:socket", generation, socketId }, socket, { keepOpen: true }, onSent);
316
+ }
240
317
  }
241
318
  catch (error) {
242
319
  settleSocket(socketId, error instanceof Error ? error : new Error(String(error)));
@@ -13,7 +13,8 @@ export function isProxyWorkerControlMessage(value) {
13
13
  Number(message.generation) <= 0) {
14
14
  return false;
15
15
  }
16
- if (message.type === "proxy-worker:socket-commit" ||
16
+ if (message.type === "proxy-worker:socket-offer" ||
17
+ message.type === "proxy-worker:socket-commit" ||
17
18
  message.type === "proxy-worker:socket-cancel") {
18
19
  return typeof message.socketId === "string" && message.socketId.length > 0;
19
20
  }
@@ -48,7 +49,8 @@ export function isProxyWorkerStatusMessage(value) {
48
49
  message.type === "proxy-worker:drained") {
49
50
  return true;
50
51
  }
51
- if (message.type === "proxy-worker:socket-accepted") {
52
+ if (message.type === "proxy-worker:socket-accepted" ||
53
+ message.type === "proxy-worker:socket-committed") {
52
54
  return typeof message.socketId === "string" && message.socketId.length > 0;
53
55
  }
54
56
  if (message.type === "proxy-worker:replacement-requested") {
@@ -4,6 +4,7 @@ const DEFAULT_READY_TIMEOUT_MS = 120_000;
4
4
  const DEFAULT_SOCKET_QUEUE_LIMIT = 1_024;
5
5
  const DEFAULT_SOCKET_QUEUE_TIMEOUT_MS = 30_000;
6
6
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
7
+ const DEFAULT_SOCKET_TRANSFER_TIMEOUT_MS = 60_000;
7
8
  const MAX_RECENT_SUPERVISOR_EVENTS = 100;
8
9
  /**
9
10
  * Owns worker generations while the caller owns the public listening socket.
@@ -105,33 +106,44 @@ export class RollingWorkerSupervisor {
105
106
  this.rejectSocket(socket, undefined, undefined, "supervisor_closed");
106
107
  return;
107
108
  }
109
+ const admission = {
110
+ socket,
111
+ // Preserve the original offer + commit allowance. Queued sockets also
112
+ // retain their existing queue wait, but no retry ever renews this budget.
113
+ deadlineAt: Date.now() +
114
+ (this.active?.handle.socketTransferTimeoutMs ??
115
+ DEFAULT_SOCKET_TRANSFER_TIMEOUT_MS),
116
+ retried: false,
117
+ };
108
118
  if (this.active &&
109
119
  this.queuedSockets.length === 0 &&
110
120
  this.active.pendingTransfers < this.options.maxPendingTransfers) {
111
- this.transferSocket(this.active, socket);
121
+ this.transferSocket(this.active, admission);
112
122
  return;
113
123
  }
114
- this.queueSocket(socket);
124
+ admission.deadlineAt += this.options.socketQueueTimeoutMs;
125
+ this.queueSocket(admission);
115
126
  this.flushQueuedSockets();
116
127
  }
117
128
  /**
118
129
  * Retain a socket only within the queue capacity and deadline, recording
119
130
  * classified rejection.
120
131
  */
121
- queueSocket(socket) {
132
+ queueSocket(admission) {
133
+ const { socket } = admission;
122
134
  if (this.queuedSockets.length >= this.options.socketQueueLimit) {
123
135
  this.rejectSocket(socket, undefined, undefined, "queue_capacity");
124
136
  return;
125
137
  }
126
138
  const queued = {
127
- socket,
139
+ ...admission,
128
140
  timeout: setTimeout(() => {
129
141
  const index = this.queuedSockets.indexOf(queued);
130
142
  if (index >= 0) {
131
143
  this.queuedSockets.splice(index, 1);
132
144
  this.rejectSocket(socket, undefined, undefined, "queue_timeout");
133
145
  }
134
- }, this.options.socketQueueTimeoutMs),
146
+ }, Math.max(0, Math.min(this.options.socketQueueTimeoutMs, admission.deadlineAt - Date.now()))),
135
147
  };
136
148
  queued.timeout.unref?.();
137
149
  this.queuedSockets.push(queued);
@@ -432,14 +444,19 @@ export class RollingWorkerSupervisor {
432
444
  break;
433
445
  }
434
446
  clearTimeout(queued.timeout);
435
- this.transferSocket(this.active, queued.socket);
447
+ this.transferSocket(this.active, queued);
436
448
  }
437
449
  }
438
450
  finally {
439
451
  this.flushingSockets = false;
440
452
  }
441
453
  }
442
- transferSocket(worker, socket) {
454
+ transferSocket(worker, admission) {
455
+ const { socket } = admission;
456
+ if (Date.now() >= admission.deadlineAt) {
457
+ this.rejectSocket(socket, worker.generation, worker.version, "admission_timeout");
458
+ return;
459
+ }
443
460
  worker.pendingTransfers += 1;
444
461
  this.scheduleTransferState();
445
462
  let settled = false;
@@ -450,7 +467,7 @@ export class RollingWorkerSupervisor {
450
467
  settled = true;
451
468
  worker.pendingTransfers = Math.max(0, worker.pendingTransfers - 1);
452
469
  if (error) {
453
- this.handleTransferFailure(worker, socket, error);
470
+ this.handleTransferFailure(worker, admission, error);
454
471
  }
455
472
  else if (this.active?.generation === worker.generation) {
456
473
  this.consecutiveOfferTimeouts = 0;
@@ -460,7 +477,7 @@ export class RollingWorkerSupervisor {
460
477
  this.scheduleTransferState();
461
478
  };
462
479
  try {
463
- worker.handle.sendSocket(worker.generation, socket, complete);
480
+ worker.handle.sendSocket(worker.generation, socket, complete, admission.deadlineAt);
464
481
  }
465
482
  catch (error) {
466
483
  complete(error instanceof Error ? error : new Error(String(error)));
@@ -487,7 +504,8 @@ export class RollingWorkerSupervisor {
487
504
  * Cancel the affected handoff and request bounded replacement without
488
505
  * killing unrelated streams.
489
506
  */
490
- handleTransferFailure(worker, socket, error) {
507
+ handleTransferFailure(worker, admission, error) {
508
+ const { socket } = admission;
491
509
  this.failedTransfers += 1;
492
510
  const detail = this.describeTransferError(error);
493
511
  this.recordEvent({
@@ -540,6 +558,25 @@ export class RollingWorkerSupervisor {
540
558
  // A failed handoff is not evidence that every connection on this worker
541
559
  // failed. Cancel only the affected socket and activate a replacement before
542
560
  // draining existing streams. Actual exits are handled by onExit below.
561
+ const replacement = this.active;
562
+ if (cancelledOffer &&
563
+ error instanceof Error &&
564
+ "socketNeverTransferred" in error &&
565
+ error.socketNeverTransferred === true &&
566
+ !lifecycle.observedExit &&
567
+ !admission.retried &&
568
+ !this.closed &&
569
+ replacement &&
570
+ replacement.generation !== worker.generation &&
571
+ replacement.pendingTransfers < this.options.maxPendingTransfers &&
572
+ Date.now() < admission.deadlineAt) {
573
+ // Only negotiated control-only offers retain every byte on this parent.
574
+ // Legacy descriptor offers can buffer bytes before commit and never retry.
575
+ // The removed IPC entry also prevents a late acceptance issuing a commit.
576
+ // Every other failure is ambiguous and must never replay request bytes.
577
+ this.transferSocket(replacement, { ...admission, retried: true });
578
+ return;
579
+ }
543
580
  this.rejectSocket(socket, worker.generation, worker.version, "transfer_failure");
544
581
  }
545
582
  /**
@@ -1,4 +1,5 @@
1
1
  import { isProxyWorkerControlMessage } from "./rollingWorkerProtocol.js";
2
+ const DEFAULT_FIRST_REQUEST_GRACE_MS = 30_000;
2
3
  function isTransferableProxySocket(handle) {
3
4
  if (!handle || typeof handle !== "object") {
4
5
  return false;
@@ -32,8 +33,33 @@ export function createSocketWorkerRuntime(server, options) {
32
33
  const sockets = new Set();
33
34
  const activeBySocket = new Map();
34
35
  const activeResponses = new Set();
36
+ const awaitingFirstRequest = new Set();
37
+ const firstRequestTimers = new Map();
38
+ const firstRequestGraceMs = Math.max(1, options?.firstRequestGraceMs ?? DEFAULT_FIRST_REQUEST_GRACE_MS);
35
39
  let draining = false;
36
40
  let drained = false;
41
+ const clearFirstRequestWait = (socket) => {
42
+ awaitingFirstRequest.delete(socket);
43
+ const timer = firstRequestTimers.get(socket);
44
+ if (timer) {
45
+ clearTimeout(timer);
46
+ firstRequestTimers.delete(socket);
47
+ }
48
+ };
49
+ const boundFirstRequestWait = (socket) => {
50
+ if (firstRequestTimers.has(socket)) {
51
+ return;
52
+ }
53
+ const timer = setTimeout(() => {
54
+ firstRequestTimers.delete(socket);
55
+ awaitingFirstRequest.delete(socket);
56
+ if (draining && sockets.has(socket) && !activeBySocket.has(socket)) {
57
+ socket.end();
58
+ }
59
+ }, firstRequestGraceMs);
60
+ timer.unref?.();
61
+ firstRequestTimers.set(socket, timer);
62
+ };
37
63
  const maybeFinishDrain = () => {
38
64
  if (draining && !drained && sockets.size === 0) {
39
65
  drained = true;
@@ -42,6 +68,7 @@ export function createSocketWorkerRuntime(server, options) {
42
68
  };
43
69
  const requestStarted = (request, response) => {
44
70
  const socket = request.socket;
71
+ clearFirstRequestWait(socket);
45
72
  activeBySocket.set(socket, (activeBySocket.get(socket) ?? 0) + 1);
46
73
  activeResponses.add(response);
47
74
  if (draining) {
@@ -75,7 +102,9 @@ export function createSocketWorkerRuntime(server, options) {
75
102
  return;
76
103
  }
77
104
  sockets.add(socket);
105
+ awaitingFirstRequest.add(socket);
78
106
  socket.once("close", () => {
107
+ clearFirstRequestWait(socket);
79
108
  sockets.delete(socket);
80
109
  activeBySocket.delete(socket);
81
110
  maybeFinishDrain();
@@ -92,7 +121,15 @@ export function createSocketWorkerRuntime(server, options) {
92
121
  response.shouldKeepAlive = false;
93
122
  }
94
123
  for (const socket of sockets) {
95
- if (!activeBySocket.has(socket)) {
124
+ if (awaitingFirstRequest.has(socket)) {
125
+ // IPC commit confirms descriptor delivery, not HTTP parser admission.
126
+ // Under host pressure a drain can otherwise end the socket before its
127
+ // first buffered request reaches the server. Preserve it for one
128
+ // bounded header-start window; the worker supervisor remains the outer
129
+ // hard shutdown bound.
130
+ boundFirstRequestWait(socket);
131
+ }
132
+ else if (!activeBySocket.has(socket)) {
96
133
  socket.end();
97
134
  }
98
135
  }
@@ -103,6 +140,11 @@ export function createSocketWorkerRuntime(server, options) {
103
140
  drain,
104
141
  close: () => {
105
142
  server.off("request", requestStarted);
143
+ for (const timer of firstRequestTimers.values()) {
144
+ clearTimeout(timer);
145
+ }
146
+ firstRequestTimers.clear();
147
+ awaitingFirstRequest.clear();
106
148
  for (const socket of sockets) {
107
149
  socket.destroy();
108
150
  }
@@ -174,6 +216,31 @@ export function attachSocketWorkerProcess(server, input) {
174
216
  };
175
217
  /** Apply supervisor messages while retaining ownership of pending and committed sockets. */
176
218
  const onMessage = (message, handle) => {
219
+ if (handle !== undefined &&
220
+ isProxyWorkerControlMessage(message) &&
221
+ message.type === "proxy-worker:socket-commit" &&
222
+ message.generation === input.generation) {
223
+ // New supervisors first offer only a socket ID. The descriptor arrives
224
+ // here at commit, so an abandoned offer cannot consume any client bytes.
225
+ if (!activated ||
226
+ gracefulDrain ||
227
+ !isTransferableProxySocket(handle) ||
228
+ pendingSockets.has(message.socketId) ||
229
+ committedSockets.has(message.socketId)) {
230
+ destroyTransferredHandle(handle);
231
+ return;
232
+ }
233
+ committedSockets.set(message.socketId, handle);
234
+ handle.once("close", () => committedSockets.delete(message.socketId));
235
+ runtime.acceptSocket(handle);
236
+ send({
237
+ type: "proxy-worker:socket-committed",
238
+ generation: input.generation,
239
+ pid: process.pid,
240
+ socketId: message.socketId,
241
+ });
242
+ return;
243
+ }
177
244
  if (message &&
178
245
  typeof message === "object" &&
179
246
  message.type === "proxy-worker:socket") {
@@ -245,7 +312,17 @@ export function attachSocketWorkerProcess(server, input) {
245
312
  }
246
313
  if (isProxyWorkerControlMessage(message) &&
247
314
  message.generation === input.generation) {
248
- if (message.type === "proxy-worker:activate") {
315
+ if (message.type === "proxy-worker:socket-offer") {
316
+ if (activated && !gracefulDrain && process.connected) {
317
+ send({
318
+ type: "proxy-worker:socket-accepted",
319
+ generation: input.generation,
320
+ pid: process.pid,
321
+ socketId: message.socketId,
322
+ });
323
+ }
324
+ }
325
+ else if (message.type === "proxy-worker:activate") {
249
326
  if (!activated) {
250
327
  try {
251
328
  input.onActivated?.();
@@ -280,6 +357,12 @@ export function attachSocketWorkerProcess(server, input) {
280
357
  committedSockets.set(message.socketId, socket);
281
358
  socket.once("close", () => committedSockets.delete(message.socketId));
282
359
  runtime.acceptSocket(socket);
360
+ send({
361
+ type: "proxy-worker:socket-committed",
362
+ generation: input.generation,
363
+ pid: process.pid,
364
+ socketId: message.socketId,
365
+ });
283
366
  }
284
367
  else {
285
368
  committedSockets.delete(message.socketId);
@@ -300,8 +383,9 @@ export function attachSocketWorkerProcess(server, input) {
300
383
  // Terminal shutdown path: the parent IPC channel is gone (disconnect) or the
301
384
  // process is being killed (SIGTERM/SIGINT). Unlike the graceful rolling drain
302
385
  // above, pending offers can no longer be committed, so they are destroyed and
303
- // the drain runs immediately — a best-effort close is correct when the process
304
- // is exiting. The zero-downtime guarantee applies to the rolling handoff
386
+ // the bounded runtime drain starts immediately. A committed socket may still
387
+ // use its first-request grace; the supervisor remains the outer hard shutdown
388
+ // bound. The zero-downtime guarantee applies to the rolling handoff
305
389
  // (control-message) path, not to process termination.
306
390
  const drain = () => {
307
391
  for (const socketId of [...pendingSockets.keys()]) {
@@ -320,6 +404,8 @@ export function attachSocketWorkerProcess(server, input) {
320
404
  pid: process.pid,
321
405
  version: input.version,
322
406
  processInstanceId: input.processInstanceId,
407
+ socketOfferProtocol: "control-before-handle",
408
+ socketCommitProtocol: "worker-ack",
323
409
  });
324
410
  return {
325
411
  ...runtime,
@@ -4,14 +4,14 @@
4
4
  * Queries the npm registry for the latest published version of
5
5
  * `@juspay/neurolink` and compares it against the currently running version.
6
6
  * Designed to be non-blocking and failure-tolerant — any error (network,
7
- * timeout, parse) silently returns `updateAvailable: false`.
7
+ * timeout, parse) returns an explicit failed check without inventing a release.
8
8
  */
9
9
  import type { UpdateCheckResult } from "../types/index.js";
10
10
  /**
11
11
  * Query npm for the latest version of `@juspay/neurolink` and compare it
12
12
  * against {@link currentVersion}.
13
13
  *
14
- * On **any** failure the function resolves (never rejects) with
15
- * `{ updateAvailable: false, latestVersion: currentVersion }`.
14
+ * Failure retains the legacy version fallback but marks it unverified. Callers
15
+ * must not persist it as a successful registry observation.
16
16
  */
17
17
  export declare function checkForUpdate(currentVersion: string): Promise<UpdateCheckResult>;
@@ -4,7 +4,7 @@
4
4
  * Queries the npm registry for the latest published version of
5
5
  * `@juspay/neurolink` and compares it against the currently running version.
6
6
  * Designed to be non-blocking and failure-tolerant — any error (network,
7
- * timeout, parse) silently returns `updateAvailable: false`.
7
+ * timeout, parse) returns an explicit failed check without inventing a release.
8
8
  */
9
9
  import { execFile as execFileCb } from "node:child_process";
10
10
  import { promisify } from "node:util";
@@ -54,15 +54,17 @@ function isNewerVersion(current, latest) {
54
54
  * Query npm for the latest version of `@juspay/neurolink` and compare it
55
55
  * against {@link currentVersion}.
56
56
  *
57
- * On **any** failure the function resolves (never rejects) with
58
- * `{ updateAvailable: false, latestVersion: currentVersion }`.
57
+ * Failure retains the legacy version fallback but marks it unverified. Callers
58
+ * must not persist it as a successful registry observation.
59
59
  */
60
60
  export async function checkForUpdate(currentVersion) {
61
- const fail = {
61
+ const fail = (checkError) => ({
62
62
  currentVersion,
63
63
  latestVersion: currentVersion,
64
64
  updateAvailable: false,
65
- };
65
+ checkSucceeded: false,
66
+ checkError,
67
+ });
66
68
  try {
67
69
  logger.debug("[UpdateChecker] Checking for updates", { currentVersion });
68
70
  const { stdout } = await execFile("npm", ["view", "@juspay/neurolink", "version", "--json"], { timeout: NPM_VIEW_TIMEOUT_MS });
@@ -72,14 +74,14 @@ export async function checkForUpdate(currentVersion) {
72
74
  logger.warn("[UpdateChecker] Unexpected npm output type", {
73
75
  type: typeof parsed,
74
76
  });
75
- return fail;
77
+ return fail("Registry returned a non-string version");
76
78
  }
77
79
  const latestVersion = parsed.trim();
78
80
  if (!parseSemVer(latestVersion)) {
79
81
  logger.warn("[UpdateChecker] Failed to parse latest version", {
80
82
  latestVersion,
81
83
  });
82
- return fail;
84
+ return fail("Registry returned an invalid version");
83
85
  }
84
86
  const updateAvailable = isNewerVersion(currentVersion, latestVersion);
85
87
  logger.debug("[UpdateChecker] Version check complete", {
@@ -87,11 +89,16 @@ export async function checkForUpdate(currentVersion) {
87
89
  latestVersion,
88
90
  updateAvailable,
89
91
  });
90
- return { currentVersion, latestVersion, updateAvailable };
92
+ return {
93
+ currentVersion,
94
+ latestVersion,
95
+ updateAvailable,
96
+ checkSucceeded: true,
97
+ };
91
98
  }
92
99
  catch (error) {
93
100
  const message = error instanceof Error ? error.message : String(error);
94
101
  logger.warn("[UpdateChecker] Update check failed", { error: message });
95
- return fail;
102
+ return fail("Registry version check failed; see updater diagnostics");
96
103
  }
97
104
  }
@@ -1,4 +1,11 @@
1
- import type { ProxyRuntimeActivity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
1
+ import type { ProxyRuntimeActivity, ProxyServingWorkerIdentity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
2
+ /**
3
+ * Parse serving identity conservatively from `/status`. A rolling status is
4
+ * trusted only when the worker-local fields agree with the supervisor snapshot.
5
+ */
6
+ export declare function parseProxyServingWorkerIdentity(payload: unknown, rollingSupervisor: boolean): ProxyServingWorkerIdentity | null;
7
+ /** A long-running update may mutate live selection only for its original worker. */
8
+ export declare function isSameProxyServingWorker(expected: ProxyServingWorkerIdentity, current: ProxyServingWorkerIdentity | null | undefined): boolean;
2
9
  /**
3
10
  * A replacement updater starts from the newly installed package. The registry
4
11
  * then reports no newer version even when its launchd parent still runs the
@@ -18,6 +25,8 @@ export declare function isProxyRuntimeSettled(activity: ProxyRuntimeActivity): b
18
25
  export declare function parseProxyRuntimeActivity(payload: unknown): ProxyRuntimeActivity | null;
19
26
  /**
20
27
  * Prefer a naturally quiet window, then briefly drain new inference traffic.
28
+ * Automatic supervisor refresh uses allowBusyDrain=false: it fences admission
29
+ * only after a quiet observation and never waits for busy workers behind it.
21
30
  * Existing requests are never interrupted; a bounded drain failure is returned
22
31
  * to the caller so admission can be reopened and retried later.
23
32
  */