@juspay/neurolink 12.14.19 → 12.14.21

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.
@@ -31,7 +31,7 @@ import { isProxyAuxiliaryRequest } from "../../proxy/proxyRequestKind.js";
31
31
  import { spawnProxySocketWorker } from "../../proxy/rollingWorkerProcess.js";
32
32
  import { PROXY_ROLLING_SUPERVISOR_ENV, PROXY_SOCKET_WORKER_ENV, } from "../../proxy/rollingWorkerProtocol.js";
33
33
  import { attachSocketWorkerProcess } from "../../proxy/socketWorkerRuntime.js";
34
- import { waitForProxyUpdateWindow } from "../../proxy/updateCoordinator.js";
34
+ import { shouldRefreshStaleSupervisor, waitForProxyUpdateWindow, } from "../../proxy/updateCoordinator.js";
35
35
  import { abandonPendingUpdate, clearUpdateDeferral, isVersionSuppressed, loadUpdateState, recordCheck, recordSuccessfulUpdate, recordUpdateDeferred, recordUpdateFailure, recordUpdateInstalled, suppressVersion, } from "../../proxy/updateState.js";
36
36
  import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../proxy/proxyEnv.js";
37
37
  import { createRequire } from "node:module";
@@ -3865,14 +3865,35 @@ export const proxyGuardCommand = {
3865
3865
  let result = await checkForUpdate(runningVersion);
3866
3866
  updateVersion = result.latestVersion;
3867
3867
  persistUpdaterState("record update check", () => recordCheck(result.latestVersion));
3868
+ let supervisorRefreshOnly = false;
3868
3869
  if (!result.updateAvailable) {
3869
- updateRetryAttempts = 0;
3870
- updateRetryVersion = null;
3871
- persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
3872
- return;
3870
+ const activeVersions = rollingSupervisor
3871
+ ? await fetchProxyRuntimeVersions(host, port).catch(() => ({}))
3872
+ : {};
3873
+ if (rollingSupervisor &&
3874
+ (!activeVersions.workerVersion || !activeVersions.supervisorVersion)) {
3875
+ logger.always(`[updater] WARNING: runtime version probe incomplete (worker=${activeVersions.workerVersion ?? "unknown"}, supervisor=${activeVersions.supervisorVersion ?? "unknown"}); stale-supervisor reconciliation deferred`);
3876
+ scheduleUpdateRetry("runtime version probe incomplete", result.latestVersion);
3877
+ return;
3878
+ }
3879
+ supervisorRefreshOnly = shouldRefreshStaleSupervisor({
3880
+ updateAvailable: result.updateAvailable,
3881
+ rollingSupervisor,
3882
+ runningVersion,
3883
+ workerVersion: activeVersions.workerVersion,
3884
+ supervisorVersion: activeVersions.supervisorVersion,
3885
+ });
3886
+ if (!supervisorRefreshOnly) {
3887
+ updateRetryAttempts = 0;
3888
+ updateRetryVersion = null;
3889
+ persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
3890
+ return;
3891
+ }
3892
+ logger.always(`[updater] worker v${runningVersion} is current; reconciling stale supervisor v${activeVersions.supervisorVersion}`);
3873
3893
  }
3874
3894
  const initiallyPendingRestart = loadUpdateState()?.pendingRestartVersion === result.latestVersion;
3875
- if (isVersionSuppressed(result.latestVersion) &&
3895
+ if (!supervisorRefreshOnly &&
3896
+ isVersionSuppressed(result.latestVersion) &&
3876
3897
  !initiallyPendingRestart) {
3877
3898
  logger.debug(`[guard] version ${result.latestVersion} is suppressed, skipping`);
3878
3899
  return;
@@ -3932,26 +3953,30 @@ export const proxyGuardCommand = {
3932
3953
  }
3933
3954
  }
3934
3955
  }
3935
- else {
3956
+ else if (!supervisorRefreshOnly) {
3936
3957
  logger.always(`[updater] rolling supervisor will keep v${runningVersion} serving during installation`);
3937
3958
  }
3938
3959
  // Refresh after waiting so a long deferral can never install a stale
3939
3960
  // target while a newer release is already available.
3940
- const refreshedResult = await checkForUpdate(runningVersion);
3941
- result = refreshedResult;
3942
- updateVersion = result.latestVersion;
3943
- persistUpdaterState("record refreshed update check", () => recordCheck(result.latestVersion));
3944
- if (!result.updateAvailable) {
3945
- persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
3946
- return;
3947
- }
3948
- const pendingRestart = loadUpdateState()?.pendingRestartVersion === result.latestVersion;
3949
- if (isVersionSuppressed(result.latestVersion) && !pendingRestart) {
3950
- logger.debug(`[guard] refreshed version ${result.latestVersion} is suppressed, skipping`);
3951
- return;
3961
+ let pendingRestart = initiallyPendingRestart;
3962
+ if (!supervisorRefreshOnly) {
3963
+ const refreshedResult = await checkForUpdate(runningVersion);
3964
+ result = refreshedResult;
3965
+ updateVersion = result.latestVersion;
3966
+ persistUpdaterState("record refreshed update check", () => recordCheck(result.latestVersion));
3967
+ if (!result.updateAvailable) {
3968
+ persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
3969
+ return;
3970
+ }
3971
+ pendingRestart =
3972
+ loadUpdateState()?.pendingRestartVersion === result.latestVersion;
3973
+ if (isVersionSuppressed(result.latestVersion) && !pendingRestart) {
3974
+ logger.debug(`[guard] refreshed version ${result.latestVersion} is suppressed, skipping`);
3975
+ return;
3976
+ }
3952
3977
  }
3953
3978
  persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
3954
- logger.always(`[updater] ${rollingSupervisor ? "rolling activation prepared" : "safe update window acquired"} for v${result.latestVersion}`);
3979
+ logger.always(`[updater] ${supervisorRefreshOnly ? "supervisor reconciliation prepared" : rollingSupervisor ? "rolling activation prepared" : "safe update window acquired"} for v${result.latestVersion}`);
3955
3980
  // 3. Install update (validate version string before passing to shell)
3956
3981
  if (!/^\d+\.\d+\.\d+$/.test(result.latestVersion)) {
3957
3982
  const message = `invalid version format: ${result.latestVersion}`;
@@ -3960,7 +3985,7 @@ export const proxyGuardCommand = {
3960
3985
  return;
3961
3986
  }
3962
3987
  const { execFileSync } = await import("node:child_process");
3963
- if (!pendingRestart) {
3988
+ if (!supervisorRefreshOnly && !pendingRestart) {
3964
3989
  const installerResolution = resolveGlobalInstaller({
3965
3990
  entryScript: process.argv[1],
3966
3991
  });
@@ -3994,35 +4019,37 @@ export const proxyGuardCommand = {
3994
4019
  return;
3995
4020
  }
3996
4021
  }
3997
- else {
4022
+ else if (!supervisorRefreshOnly) {
3998
4023
  logger.always(`[updater] resuming pending restart for already-installed v${result.latestVersion}`);
3999
4024
  }
4000
4025
  // 4. Refresh and validate the stable trampoline. The plist already
4001
4026
  // points at this path, so it must not be unloaded or rewritten here.
4002
- try {
4003
- writeTrampoline();
4004
- const validation = await validateInstalledVersion({
4005
- binPath: TRAMPOLINE_PATH,
4006
- expectedVersion: result.latestVersion,
4007
- });
4008
- if (validation.version !== result.latestVersion) {
4009
- const message = `trampoline validation failed after ${validation.attempts} attempts: ${validation.failure ?? "unknown failure"}`;
4010
- logger.always(`[updater] WARNING: ${message}; restart deferred`);
4027
+ if (!supervisorRefreshOnly) {
4028
+ try {
4029
+ writeTrampoline();
4030
+ const validation = await validateInstalledVersion({
4031
+ binPath: TRAMPOLINE_PATH,
4032
+ expectedVersion: result.latestVersion,
4033
+ });
4034
+ if (validation.version !== result.latestVersion) {
4035
+ const message = `trampoline validation failed after ${validation.attempts} attempts: ${validation.failure ?? "unknown failure"}`;
4036
+ logger.always(`[updater] WARNING: ${message}; restart deferred`);
4037
+ persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
4038
+ persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
4039
+ return;
4040
+ }
4041
+ persistUpdaterState("record installed update", () => recordUpdateInstalled(result.latestVersion));
4042
+ logger.always(`[updater] trampoline validated at v${validation.version} after ${validation.attempts} attempt(s)`);
4043
+ }
4044
+ catch (trampolineError) {
4045
+ const message = trampolineError instanceof Error
4046
+ ? trampolineError.message
4047
+ : String(trampolineError);
4048
+ logger.always(`[updater] WARNING: failed to refresh trampoline; refusing restart: ${message}`);
4011
4049
  persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
4012
4050
  persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
4013
4051
  return;
4014
4052
  }
4015
- persistUpdaterState("record installed update", () => recordUpdateInstalled(result.latestVersion));
4016
- logger.always(`[updater] trampoline validated at v${validation.version} after ${validation.attempts} attempt(s)`);
4017
- }
4018
- catch (trampolineError) {
4019
- const message = trampolineError instanceof Error
4020
- ? trampolineError.message
4021
- : String(trampolineError);
4022
- logger.always(`[updater] WARNING: failed to refresh trampoline; refusing restart: ${message}`);
4023
- persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
4024
- persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
4025
- return;
4026
4053
  }
4027
4054
  if (guardStopping || getProcessStatus(parentPid) === "not_running") {
4028
4055
  return;
@@ -4200,11 +4227,11 @@ export const proxyGuardCommand = {
4200
4227
  process.exit(0);
4201
4228
  }
4202
4229
  else {
4203
- logger.always(`[updater] WARNING: proxy unhealthy after update to ${result.latestVersion}`);
4230
+ logger.always(`[updater] WARNING: proxy unhealthy after ${supervisorRefreshOnly ? "supervisor reconciliation at" : "update to"} ${result.latestVersion}`);
4204
4231
  let failureMessage = rollingFailure
4205
4232
  ? `rolling candidate failed: ${rollingFailure}`
4206
4233
  : `proxy did not report v${result.latestVersion} healthy within ${UPDATE_TIMEOUT_MS}ms`;
4207
- if (rollingSupervisor) {
4234
+ if (rollingSupervisor && !supervisorRefreshOnly) {
4208
4235
  try {
4209
4236
  const rollbackResolution = resolveGlobalInstaller({
4210
4237
  entryScript: process.argv[1],
@@ -4244,8 +4271,13 @@ export const proxyGuardCommand = {
4244
4271
  }
4245
4272
  }
4246
4273
  persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "health", failureMessage));
4247
- persistUpdaterState("abandon unhealthy pending update", () => abandonPendingUpdate(result.latestVersion));
4248
- persistUpdaterState("suppress unhealthy update", () => suppressVersion(result.latestVersion, "unhealthy_after_restart"));
4274
+ if (supervisorRefreshOnly) {
4275
+ scheduleUpdateRetry("stale supervisor reconciliation health unavailable", result.latestVersion);
4276
+ }
4277
+ else {
4278
+ persistUpdaterState("abandon unhealthy pending update", () => abandonPendingUpdate(result.latestVersion));
4279
+ persistUpdaterState("suppress unhealthy update", () => suppressVersion(result.latestVersion, "unhealthy_after_restart"));
4280
+ }
4249
4281
  updateRestartInProgress = false;
4250
4282
  }
4251
4283
  }
@@ -1,6 +1,12 @@
1
1
  import { Worker } from "node:worker_threads";
2
2
  const MAX_PENDING = 64;
3
3
  const MAX_PENDING_BYTES = 32 * 1024 * 1024;
4
+ // OTel publication is asynchronous. Absorb one additional bounded burst
5
+ // without expanding the active structured-clone pool or blocking model
6
+ // responses; sustained overload still expires with an explicit index failure.
7
+ const MAX_OTEL_WAITING = 64;
8
+ const MAX_OTEL_WAITING_BYTES = 32 * 1024 * 1024;
9
+ const OTEL_ADMISSION_WAIT_MS = 20_000;
4
10
  // Bound retained UTF-16 strings rather than a 3x UTF-8 guess. This accommodates
5
11
  // the observed 7.3 MB JSON requests while retaining a 32 MiB aggregate pool.
6
12
  const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
@@ -20,9 +26,99 @@ const snapshot = {
20
26
  maxPendingBytes: MAX_PENDING_BYTES,
21
27
  highWaterPending: 0,
22
28
  highWaterBytes: 0,
29
+ waiting: 0,
30
+ waitingBytes: 0,
31
+ maxWaiting: MAX_OTEL_WAITING,
32
+ maxWaitingBytes: MAX_OTEL_WAITING_BYTES,
33
+ highWaterWaiting: 0,
34
+ highWaterWaitingBytes: 0,
35
+ admissionWaits: 0,
36
+ admissionTimeouts: 0,
23
37
  rejectionReasons: {},
24
38
  };
25
39
  const pending = new Map();
40
+ const otelAdmissionWaiters = [];
41
+ let admissionRetryTimer;
42
+ /** Atomically reserve the active clone/publication lease. */
43
+ function reserveCapture(bytes) {
44
+ if (Date.now() < retryAfter ||
45
+ snapshot.pending >= MAX_PENDING ||
46
+ snapshot.pendingBytes + bytes > MAX_PENDING_BYTES) {
47
+ return false;
48
+ }
49
+ snapshot.pending += 1;
50
+ snapshot.pendingBytes += bytes;
51
+ snapshot.highWaterPending = Math.max(snapshot.highWaterPending, snapshot.pending);
52
+ snapshot.highWaterBytes = Math.max(snapshot.highWaterBytes, snapshot.pendingBytes);
53
+ return true;
54
+ }
55
+ /** Admit every compatible OTel waiter as active leases settle. */
56
+ function drainOtelAdmissionWaiters() {
57
+ if (!otelAdmissionWaiters.length) {
58
+ return;
59
+ }
60
+ if (Date.now() < retryAfter) {
61
+ if (!admissionRetryTimer) {
62
+ admissionRetryTimer = setTimeout(() => {
63
+ admissionRetryTimer = undefined;
64
+ drainOtelAdmissionWaiters();
65
+ }, Math.max(1, retryAfter - Date.now()));
66
+ admissionRetryTimer.unref?.();
67
+ }
68
+ return;
69
+ }
70
+ for (let index = 0; index < otelAdmissionWaiters.length;) {
71
+ if (snapshot.pending >= MAX_PENDING) {
72
+ break;
73
+ }
74
+ const waiter = otelAdmissionWaiters[index];
75
+ if (!reserveCapture(waiter.bytes)) {
76
+ // Byte pressure can block a large capture while a later small capture
77
+ // still fits. Keep the large capture's original timeout and scan the
78
+ // rest of the bounded queue once.
79
+ index += 1;
80
+ continue;
81
+ }
82
+ otelAdmissionWaiters.splice(index, 1);
83
+ clearTimeout(waiter.timer);
84
+ snapshot.waiting -= 1;
85
+ snapshot.waitingBytes -= waiter.bytes;
86
+ waiter.resolve("admitted");
87
+ }
88
+ }
89
+ /** Retain bounded OTel overflow instead of dropping a transient burst. */
90
+ function waitForOtelAdmission(bytes) {
91
+ if (snapshot.waiting >= MAX_OTEL_WAITING ||
92
+ snapshot.waitingBytes + bytes > MAX_OTEL_WAITING_BYTES) {
93
+ return Promise.resolve("queue_full");
94
+ }
95
+ snapshot.admissionWaits += 1;
96
+ snapshot.waiting += 1;
97
+ snapshot.waitingBytes += bytes;
98
+ snapshot.highWaterWaiting = Math.max(snapshot.highWaterWaiting, snapshot.waiting);
99
+ snapshot.highWaterWaitingBytes = Math.max(snapshot.highWaterWaitingBytes, snapshot.waitingBytes);
100
+ return new Promise((resolve) => {
101
+ const timer = setTimeout(() => {
102
+ const index = otelAdmissionWaiters.findIndex((waiter) => waiter.timer === timer);
103
+ if (index < 0) {
104
+ return;
105
+ }
106
+ otelAdmissionWaiters.splice(index, 1);
107
+ snapshot.waiting -= 1;
108
+ snapshot.waitingBytes -= bytes;
109
+ snapshot.admissionTimeouts += 1;
110
+ resolve("timeout");
111
+ drainOtelAdmissionWaiters();
112
+ }, OTEL_ADMISSION_WAIT_MS);
113
+ timer.unref?.();
114
+ otelAdmissionWaiters.push({ bytes, resolve, timer });
115
+ });
116
+ }
117
+ function releaseCapture(bytes) {
118
+ snapshot.pending -= 1;
119
+ snapshot.pendingBytes -= bytes;
120
+ drainOtelAdmissionWaiters();
121
+ }
26
122
  // Bound traversal as well as the structured clone sent to the worker. Never
27
123
  // invoke getters/toJSON or stringify a large body on the serving event loop.
28
124
  /**
@@ -135,6 +231,7 @@ function getWorker() {
135
231
  /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
136
232
  export async function captureProxyBody(entry, logDir, consume) {
137
233
  snapshot.attempted += 1;
234
+ const queuedAt = Date.now();
138
235
  // An OTel capture can use the existing byte pool when it is otherwise idle.
139
236
  // Raising the slot count does not raise the aggregate retained-memory bound.
140
237
  const maxEntryBytes = logDir === null ? MAX_PENDING_BYTES : MAX_ENTRY_BYTES;
@@ -155,16 +252,23 @@ export async function captureProxyBody(entry, logDir, consume) {
155
252
  ? error.message
156
253
  : "body_capture_unsupported_value";
157
254
  }
158
- if (bytes > maxEntryBytes ||
159
- snapshot.pending >= MAX_PENDING ||
160
- snapshot.pendingBytes + bytes > MAX_PENDING_BYTES ||
161
- Date.now() < retryAfter) {
255
+ let admission = "queue_full";
256
+ if (bytes <= maxEntryBytes && Date.now() >= retryAfter) {
257
+ admission = reserveCapture(bytes)
258
+ ? "admitted"
259
+ : logDir === null
260
+ ? await waitForOtelAdmission(bytes)
261
+ : "queue_full";
262
+ }
263
+ if (admission !== "admitted") {
162
264
  snapshot.rejected += 1;
163
265
  const error = bytes > maxEntryBytes
164
266
  ? (admissionError ?? "body_capture_entry_too_large")
165
267
  : Date.now() < retryAfter
166
268
  ? "body_worker_backoff"
167
- : "body_capture_queue_full";
269
+ : admission === "timeout"
270
+ ? "body_capture_admission_timeout"
271
+ : "body_capture_queue_full";
168
272
  snapshot.lastError = error;
169
273
  snapshot.lastRejectedAt = new Date().toISOString();
170
274
  snapshot.rejectionReasons[error] =
@@ -194,8 +298,9 @@ export async function captureProxyBody(entry, logDir, consume) {
194
298
  current = getWorker();
195
299
  }
196
300
  catch {
197
- snapshot.failed += 1;
198
301
  retryAfter = Date.now() + 5_000;
302
+ releaseCapture(bytes);
303
+ snapshot.failed += 1;
199
304
  return consume({
200
305
  error: "body_worker_start_failed",
201
306
  stored: { bodyWriteFailed: true },
@@ -215,8 +320,7 @@ export async function captureProxyBody(entry, logDir, consume) {
215
320
  result.error ??= "body_capture_publication_failed";
216
321
  })
217
322
  .finally(() => {
218
- snapshot.pending -= 1;
219
- snapshot.pendingBytes -= bytes;
323
+ releaseCapture(bytes);
220
324
  if (result.error || result.stored.bodyWriteFailed) {
221
325
  snapshot.failed += 1;
222
326
  }
@@ -230,13 +334,9 @@ export async function captureProxyBody(entry, logDir, consume) {
230
334
  });
231
335
  },
232
336
  });
233
- snapshot.pending += 1;
234
- snapshot.highWaterPending = Math.max(snapshot.highWaterPending, snapshot.pending);
235
- snapshot.pendingBytes += bytes;
236
- snapshot.highWaterBytes = Math.max(snapshot.highWaterBytes, snapshot.pendingBytes);
237
337
  current.ref();
238
338
  try {
239
- current.postMessage({ id, entry, logDir, queuedAt: Date.now() });
339
+ current.postMessage({ id, entry, logDir, queuedAt });
240
340
  }
241
341
  catch {
242
342
  settle(id, {
@@ -259,6 +359,9 @@ export const __bodyCaptureWorkerTestHooks = {
259
359
  * Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
260
360
  */
261
361
  async reset(url) {
362
+ if (otelAdmissionWaiters.length) {
363
+ throw new Error("Cannot reset body capture worker with admission waiters");
364
+ }
262
365
  if (worker) {
263
366
  const current = worker;
264
367
  failWorker(current, "body_worker_test_reset");
@@ -266,6 +369,10 @@ export const __bodyCaptureWorkerTestHooks = {
266
369
  }
267
370
  workerUrl = url;
268
371
  retryAfter = 0;
372
+ if (admissionRetryTimer) {
373
+ clearTimeout(admissionRetryTimer);
374
+ admissionRetryTimer = undefined;
375
+ }
269
376
  Object.assign(snapshot, {
270
377
  attempted: 0,
271
378
  completed: 0,
@@ -277,6 +384,12 @@ export const __bodyCaptureWorkerTestHooks = {
277
384
  lastRejectedAt: undefined,
278
385
  highWaterPending: 0,
279
386
  highWaterBytes: 0,
387
+ waiting: 0,
388
+ waitingBytes: 0,
389
+ highWaterWaiting: 0,
390
+ highWaterWaitingBytes: 0,
391
+ admissionWaits: 0,
392
+ admissionTimeouts: 0,
280
393
  rejectionReasons: {},
281
394
  });
282
395
  },
@@ -10,7 +10,10 @@ import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry, Pr
10
10
  * Expose independent metadata-sink and body-capture counters for incident reconciliation.
11
11
  */
12
12
  export declare function getRequestLoggerSnapshot(): ProxyRequestLoggerSnapshot;
13
- /** Wait, up to a bounded deadline, for admitted request/body writes to settle. */
13
+ /**
14
+ * Wait through bounded OTel admission, worker processing, and publication
15
+ * deadlines so a clean shutdown does not abandon an accepted overflow capture.
16
+ */
14
17
  export declare function flushRequestLogs(timeoutMs?: number): Promise<void>;
15
18
  /**
16
19
  * Initialize private request logs and preserve required lifecycle admission on startup failures.
@@ -26,6 +26,10 @@ let logDir = null;
26
26
  let logEnabled = false;
27
27
  const pendingLogOperations = new Set();
28
28
  const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
29
+ // launchd gives the service 45 seconds after SIGTERM. Leave fifteen seconds
30
+ // for listener drain and exporter shutdown instead of advertising a flush
31
+ // deadline the service manager can terminate before it reaches.
32
+ const REQUEST_LOG_FLUSH_TIMEOUT_MS = PROXY_BODY_CAPTURE_DEADLINE_MS + 2 * REQUEST_LOG_IO_TIMEOUT_MS;
29
33
  const MAX_PENDING_METADATA_RECORDS = 4_096;
30
34
  const appendChains = new Map();
31
35
  let appendMetadataFile = writeFile;
@@ -114,9 +118,11 @@ function trackLogOperation(operation) {
114
118
  void operation.then(() => pendingLogOperations.delete(operation), () => pendingLogOperations.delete(operation));
115
119
  return operation;
116
120
  }
117
- /** Wait, up to a bounded deadline, for admitted request/body writes to settle. */
118
- export async function flushRequestLogs(timeoutMs = PROXY_BODY_CAPTURE_DEADLINE_MS +
119
- 2 * REQUEST_LOG_IO_TIMEOUT_MS) {
121
+ /**
122
+ * Wait through bounded OTel admission, worker processing, and publication
123
+ * deadlines so a clean shutdown does not abandon an accepted overflow capture.
124
+ */
125
+ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_FLUSH_TIMEOUT_MS) {
120
126
  const deadline = Date.now() + Math.max(1, timeoutMs);
121
127
  while (pendingLogOperations.size > 0) {
122
128
  const admitted = [...pendingLogOperations];
@@ -130,6 +136,7 @@ export async function flushRequestLogs(timeoutMs = PROXY_BODY_CAPTURE_DEADLINE_M
130
136
  }
131
137
  /** @internal Test-only hook for exercising shutdown behavior without real I/O. */
132
138
  export const __requestLoggerTestHooks = {
139
+ defaultFlushTimeoutMs: REQUEST_LOG_FLUSH_TIMEOUT_MS,
133
140
  pendingOperationCount: () => pendingLogOperations.size,
134
141
  trackLogOperation,
135
142
  setAppendFileForTests: (writer) => {
@@ -1,4 +1,17 @@
1
1
  import type { ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
2
+ /**
3
+ * A replacement updater starts from the newly installed package. The registry
4
+ * then reports no newer version even when its launchd parent still runs the
5
+ * old package, so parent reconciliation must be decided from both live
6
+ * generations instead of `updateAvailable` alone.
7
+ */
8
+ export declare function shouldRefreshStaleSupervisor(options: {
9
+ updateAvailable: boolean;
10
+ rollingSupervisor: boolean;
11
+ runningVersion: string;
12
+ workerVersion?: string;
13
+ supervisorVersion?: string;
14
+ }): boolean;
2
15
  /**
3
16
  * Prefer a naturally quiet window, then briefly drain new inference traffic.
4
17
  * Existing requests are never interrupted; a bounded drain failure is returned
@@ -1,3 +1,16 @@
1
+ /**
2
+ * A replacement updater starts from the newly installed package. The registry
3
+ * then reports no newer version even when its launchd parent still runs the
4
+ * old package, so parent reconciliation must be decided from both live
5
+ * generations instead of `updateAvailable` alone.
6
+ */
7
+ export function shouldRefreshStaleSupervisor(options) {
8
+ return (!options.updateAvailable &&
9
+ options.rollingSupervisor &&
10
+ options.workerVersion === options.runningVersion &&
11
+ typeof options.supervisorVersion === "string" &&
12
+ options.supervisorVersion !== options.runningVersion);
13
+ }
1
14
  function isQuiet(activity, quietThresholdMs, nowMs) {
2
15
  if (activity.activeRequests > 0) {
3
16
  return false;
@@ -1131,6 +1131,15 @@ export type TextGenerationOptions = {
1131
1131
  */
1132
1132
  images?: Array<Buffer | string | ImageWithAltText>;
1133
1133
  pdfFiles?: Array<Buffer | string>;
1134
+ /**
1135
+ * CSV files to inline as tabular text, with tool instructions appended.
1136
+ *
1137
+ * Declared here because `processExplicitCsvFiles` has always read it and
1138
+ * the internal `GenerateOptions` has always carried it — it was missing
1139
+ * only from the public type, so callers reaching the shipped behaviour had
1140
+ * to widen the type themselves to do it.
1141
+ */
1142
+ csvFiles?: Array<Buffer | string>;
1134
1143
  files?: Array<Buffer | string | FileWithMetadata>;
1135
1144
  /** Director Mode segments (2-10). When provided, Director Mode is activated. */
1136
1145
  segments?: DirectorSegment[];
@@ -669,7 +669,7 @@ export type ProxyBodyCaptureWorkerSnapshot = {
669
669
  rejectionReasons: Record<string, number>;
670
670
  lastError?: string;
671
671
  lastRejectedAt?: string;
672
- };
672
+ } & ProxyBodyCaptureWaitingSnapshot;
673
673
  /** Collector transport evidence; acknowledgement does not prove backend storage. */
674
674
  export type ProxyBodyDeliveryResult = {
675
675
  status: "transport_acknowledged" | "export_unconfirmed" | "rejected" | "partial";
@@ -4000,3 +4000,15 @@ export type ProxyLogTraceContext = {
4000
4000
  spanId: string;
4001
4001
  traceFlags: number;
4002
4002
  };
4003
+ /** OTel-only captures waiting for bounded worker capacity. */
4004
+ type ProxyBodyCaptureWaitingSnapshot = {
4005
+ waiting: number;
4006
+ waitingBytes: number;
4007
+ maxWaiting: number;
4008
+ maxWaitingBytes: number;
4009
+ highWaterWaiting: number;
4010
+ highWaterWaitingBytes: number;
4011
+ admissionWaits: number;
4012
+ admissionTimeouts: number;
4013
+ };
4014
+ export {};
@@ -187,6 +187,10 @@ export declare class FileDetector {
187
187
  * Load file from URL with automatic retry on transient network errors
188
188
  */
189
189
  private static loadFromURL;
190
+ /**
191
+ * Fetch a URL once and retain its response MIME type for detection.
192
+ */
193
+ private static loadUrl;
190
194
  /**
191
195
  * Load file from filesystem path
192
196
  */