@juspay/neurolink 12.14.3 → 12.14.5
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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +370 -370
- package/dist/cli/commands/proxy.js +11 -3
- package/dist/mcp/caching/toolCache.d.ts +30 -0
- package/dist/mcp/caching/toolCache.js +52 -3
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +19 -4
- package/dist/proxy/bodyCaptureWorker.js +35 -13
- package/dist/proxy/otelLogSink.d.ts +22 -1
- package/dist/proxy/otelLogSink.js +247 -8
- package/dist/proxy/proxyActivity.d.ts +4 -2
- package/dist/proxy/proxyActivity.js +20 -1
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +53 -15
- package/dist/types/proxy.d.ts +29 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/check-proxy-telemetry.mjs +20 -7
- package/scripts/observability/query-proxy-history.mjs +193 -0
|
@@ -1079,6 +1079,8 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1079
1079
|
requestMetadata.set(c.req.raw, metadata);
|
|
1080
1080
|
const stopObservingFinalLog = observeProxyFinalLog(metadata.requestId, (entry) => {
|
|
1081
1081
|
metadata.terminalResult = entry;
|
|
1082
|
+
}, (entry) => {
|
|
1083
|
+
metadata.lastUpstreamAttempt = { ...entry };
|
|
1082
1084
|
});
|
|
1083
1085
|
const finishActivity = metadata.rejectForUpdate
|
|
1084
1086
|
? () => undefined
|
|
@@ -1306,7 +1308,8 @@ export async function createProxyStartApp(params) {
|
|
|
1306
1308
|
const recordRuntimeError = async (metadata, status, errorType, errorMessage, options) => {
|
|
1307
1309
|
const clientMessage = options?.clientMessage ?? errorMessage;
|
|
1308
1310
|
const clientErrorType = options?.clientErrorType ?? errorType;
|
|
1309
|
-
|
|
1311
|
+
const attempt = metadata.lastUpstreamAttempt;
|
|
1312
|
+
recordFinalError(status, attempt?.account ?? PROXY_INTERNAL_ACCOUNT_LABEL, attempt?.accountType ?? PROXY_INTERNAL_ACCOUNT_TYPE, {
|
|
1310
1313
|
requestId: metadata.requestId,
|
|
1311
1314
|
errorType,
|
|
1312
1315
|
errorCode: options?.errorCode,
|
|
@@ -1322,8 +1325,13 @@ export async function createProxyStartApp(params) {
|
|
|
1322
1325
|
model: metadata.model,
|
|
1323
1326
|
stream: metadata.stream,
|
|
1324
1327
|
toolCount: metadata.toolCount,
|
|
1325
|
-
account: "",
|
|
1326
|
-
accountType: "proxy-runtime",
|
|
1328
|
+
account: attempt?.account ?? "",
|
|
1329
|
+
accountType: attempt?.accountType ?? "proxy-runtime",
|
|
1330
|
+
accountKey: attempt?.accountKey,
|
|
1331
|
+
provider: attempt?.provider,
|
|
1332
|
+
transportScope: attempt?.transportScope,
|
|
1333
|
+
traceId: attempt?.traceId,
|
|
1334
|
+
spanId: attempt?.spanId,
|
|
1327
1335
|
responseStatus: status,
|
|
1328
1336
|
responseTimeMs: Date.now() - metadata.startedAt,
|
|
1329
1337
|
errorType,
|
|
@@ -38,10 +38,18 @@ export declare class ToolCache<T = unknown> extends EventEmitter {
|
|
|
38
38
|
constructor(config: McpCacheConfig);
|
|
39
39
|
/**
|
|
40
40
|
* Get a value from the cache
|
|
41
|
+
*
|
|
42
|
+
* Returns an isolated copy of the stored value (see `cloneCachedValue`), so
|
|
43
|
+
* a caller mutating what it gets back cannot corrupt the entry for later
|
|
44
|
+
* hits or for other concurrent callers of the same key.
|
|
41
45
|
*/
|
|
42
46
|
get(key: string): T | undefined;
|
|
43
47
|
/**
|
|
44
48
|
* Set a value in the cache
|
|
49
|
+
*
|
|
50
|
+
* Stores an isolated copy of `value` (see `cloneCachedValue`), so mutating
|
|
51
|
+
* the caller's original object after this call cannot reach into the
|
|
52
|
+
* cache entry.
|
|
45
53
|
*/
|
|
46
54
|
set(key: string, value: T, ttl?: number): void;
|
|
47
55
|
/**
|
|
@@ -89,6 +97,28 @@ export declare class ToolCache<T = unknown> extends EventEmitter {
|
|
|
89
97
|
* Stop the auto-cleanup timer
|
|
90
98
|
*/
|
|
91
99
|
destroy(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Isolate a value crossing the cache boundary (on write into the entry,
|
|
102
|
+
* and on read back out of it) so no two callers — nor a caller and the
|
|
103
|
+
* stored entry itself — ever share object identity.
|
|
104
|
+
*
|
|
105
|
+
* Without this, `set()` stored the caller's object by reference and
|
|
106
|
+
* `get()` returned `entry.value` by the same reference on every hit: one
|
|
107
|
+
* caller mutating a result it got back (e.g. an in-place truncation or
|
|
108
|
+
* normalization pass) silently corrupted the entry for every later
|
|
109
|
+
* caller of the same key for the rest of the TTL.
|
|
110
|
+
*
|
|
111
|
+
* `structuredClone` is the primary path — it is a deep copy, has no
|
|
112
|
+
* caller-visible side effects, and (unlike a JSON round-trip) tolerates
|
|
113
|
+
* circular references, which a sufficiently deep or recursive tool
|
|
114
|
+
* result could contain. It throws on values it cannot clone (functions,
|
|
115
|
+
* some non-plain class instances); the JSON round-trip fallback covers
|
|
116
|
+
* that case for the plain-data shapes MCP tool results actually have
|
|
117
|
+
* (text/JSON content arrays), at the cost of silently dropping
|
|
118
|
+
* `undefined`, functions and symbol keys — acceptable for a cache that
|
|
119
|
+
* only ever holds serializable tool results.
|
|
120
|
+
*/
|
|
121
|
+
private cloneCachedValue;
|
|
92
122
|
private getFullKey;
|
|
93
123
|
private isExpired;
|
|
94
124
|
/**
|
|
@@ -60,6 +60,10 @@ export class ToolCache extends EventEmitter {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Get a value from the cache
|
|
63
|
+
*
|
|
64
|
+
* Returns an isolated copy of the stored value (see `cloneCachedValue`), so
|
|
65
|
+
* a caller mutating what it gets back cannot corrupt the entry for later
|
|
66
|
+
* hits or for other concurrent callers of the same key.
|
|
63
67
|
*/
|
|
64
68
|
get(key) {
|
|
65
69
|
const fullKey = this.getFullKey(key);
|
|
@@ -83,11 +87,24 @@ export class ToolCache extends EventEmitter {
|
|
|
83
87
|
entry.accessCount++;
|
|
84
88
|
this.stats.hits++;
|
|
85
89
|
this.updateHitRate();
|
|
86
|
-
this.
|
|
87
|
-
|
|
90
|
+
const returnedValue = this.cloneCachedValue(entry.value);
|
|
91
|
+
if (this.listenerCount("hit") > 0) {
|
|
92
|
+
// Listeners get their own copy. `emit` is synchronous, so a listener
|
|
93
|
+
// that mutates `event.value` would otherwise be mutating the very object
|
|
94
|
+
// the caller is about to receive.
|
|
95
|
+
this.emit("hit", {
|
|
96
|
+
key: fullKey,
|
|
97
|
+
value: this.cloneCachedValue(entry.value),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return returnedValue;
|
|
88
101
|
}
|
|
89
102
|
/**
|
|
90
103
|
* Set a value in the cache
|
|
104
|
+
*
|
|
105
|
+
* Stores an isolated copy of `value` (see `cloneCachedValue`), so mutating
|
|
106
|
+
* the caller's original object after this call cannot reach into the
|
|
107
|
+
* cache entry.
|
|
91
108
|
*/
|
|
92
109
|
set(key, value, ttl) {
|
|
93
110
|
const fullKey = this.getFullKey(key);
|
|
@@ -98,7 +115,7 @@ export class ToolCache extends EventEmitter {
|
|
|
98
115
|
this.evictOne();
|
|
99
116
|
}
|
|
100
117
|
const entry = {
|
|
101
|
-
value,
|
|
118
|
+
value: this.cloneCachedValue(value),
|
|
102
119
|
expires: now + effectiveTtl,
|
|
103
120
|
createdAt: now,
|
|
104
121
|
accessedAt: now,
|
|
@@ -250,6 +267,38 @@ export class ToolCache extends EventEmitter {
|
|
|
250
267
|
this.clear();
|
|
251
268
|
}
|
|
252
269
|
// ==================== Private Methods ====================
|
|
270
|
+
/**
|
|
271
|
+
* Isolate a value crossing the cache boundary (on write into the entry,
|
|
272
|
+
* and on read back out of it) so no two callers — nor a caller and the
|
|
273
|
+
* stored entry itself — ever share object identity.
|
|
274
|
+
*
|
|
275
|
+
* Without this, `set()` stored the caller's object by reference and
|
|
276
|
+
* `get()` returned `entry.value` by the same reference on every hit: one
|
|
277
|
+
* caller mutating a result it got back (e.g. an in-place truncation or
|
|
278
|
+
* normalization pass) silently corrupted the entry for every later
|
|
279
|
+
* caller of the same key for the rest of the TTL.
|
|
280
|
+
*
|
|
281
|
+
* `structuredClone` is the primary path — it is a deep copy, has no
|
|
282
|
+
* caller-visible side effects, and (unlike a JSON round-trip) tolerates
|
|
283
|
+
* circular references, which a sufficiently deep or recursive tool
|
|
284
|
+
* result could contain. It throws on values it cannot clone (functions,
|
|
285
|
+
* some non-plain class instances); the JSON round-trip fallback covers
|
|
286
|
+
* that case for the plain-data shapes MCP tool results actually have
|
|
287
|
+
* (text/JSON content arrays), at the cost of silently dropping
|
|
288
|
+
* `undefined`, functions and symbol keys — acceptable for a cache that
|
|
289
|
+
* only ever holds serializable tool results.
|
|
290
|
+
*/
|
|
291
|
+
cloneCachedValue(value) {
|
|
292
|
+
if (value === null || typeof value !== "object") {
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return structuredClone(value);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return JSON.parse(JSON.stringify(value));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
253
302
|
getFullKey(key) {
|
|
254
303
|
return this.config.namespace ? `${this.config.namespace}:${key}` : key;
|
|
255
304
|
}
|
|
@@ -6,6 +6,7 @@ import { gzip as gzipCallback } from "node:zlib";
|
|
|
6
6
|
const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
|
|
7
7
|
/** Maximum redacted body bytes persisted per capture entry. */
|
|
8
8
|
const MAX_CAPTURED_BODY_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_OTEL_CAPTURED_BODY_BYTES = 8 * 1024 * 1024;
|
|
9
10
|
const BODY_TRUNCATION_MARKER = "\n...[TRUNCATED]";
|
|
10
11
|
const gzip = promisify(gzipCallback);
|
|
11
12
|
/** Headers whose values must always be redacted. */
|
|
@@ -130,12 +131,15 @@ export function splitUtf8StringByBytes(input, maxBytes) {
|
|
|
130
131
|
* Apply structural redaction before enforcing the per-artifact byte
|
|
131
132
|
* ceiling.
|
|
132
133
|
*/
|
|
133
|
-
function prepareRedactedBody(body) {
|
|
134
|
+
function prepareRedactedBody(body, maxBytes = MAX_CAPTURED_BODY_BYTES) {
|
|
134
135
|
const redacted = redactBody(body);
|
|
135
136
|
if (redacted === undefined) {
|
|
136
137
|
return { truncated: false };
|
|
137
138
|
}
|
|
138
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
...truncateUtf8String(redacted, maxBytes),
|
|
141
|
+
originalBytes: utf8ByteLength(redacted),
|
|
142
|
+
};
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
141
145
|
* Write a private gzip artifact with a unique name and return its
|
|
@@ -202,7 +206,8 @@ export function redactProxyHeadersForLogging(headers) {
|
|
|
202
206
|
*/
|
|
203
207
|
export async function processProxyBodyCapture(entry, logDir) {
|
|
204
208
|
const headers = redactHeaders(entry.headers);
|
|
205
|
-
const
|
|
209
|
+
const limit = logDir === null ? MAX_OTEL_CAPTURED_BODY_BYTES : MAX_CAPTURED_BODY_BYTES;
|
|
210
|
+
const prepared = prepareRedactedBody(entry.body, limit);
|
|
206
211
|
if (logDir === null) {
|
|
207
212
|
return {
|
|
208
213
|
headers,
|
|
@@ -210,6 +215,9 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
210
215
|
redactedBody: prepared.value,
|
|
211
216
|
redactedBodyBytes: prepared.bytes,
|
|
212
217
|
bodyTruncated: prepared.truncated,
|
|
218
|
+
bodyCaptureLimitBytes: limit,
|
|
219
|
+
originalRedactedBodyBytes: prepared.originalBytes,
|
|
220
|
+
bodySha256: prepared.value === undefined ? undefined : sha256(prepared.value),
|
|
213
221
|
},
|
|
214
222
|
};
|
|
215
223
|
}
|
|
@@ -225,5 +233,12 @@ export async function processProxyBodyCapture(entry, logDir) {
|
|
|
225
233
|
bodyWriteFailed: true,
|
|
226
234
|
};
|
|
227
235
|
}
|
|
228
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
headers,
|
|
238
|
+
stored: {
|
|
239
|
+
...stored,
|
|
240
|
+
bodyCaptureLimitBytes: limit,
|
|
241
|
+
originalRedactedBodyBytes: prepared.originalBytes,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
229
244
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
2
|
const MAX_PENDING = 16;
|
|
3
3
|
const MAX_PENDING_BYTES = 32 * 1024 * 1024;
|
|
4
|
-
|
|
4
|
+
// Bound retained UTF-16 strings rather than a 3x UTF-8 guess. This accommodates
|
|
5
|
+
// the observed 7.3 MB JSON requests while retaining a 32 MiB aggregate pool.
|
|
6
|
+
const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
5
7
|
export const PROXY_BODY_CAPTURE_DEADLINE_MS = 20_000;
|
|
6
8
|
let worker;
|
|
7
9
|
let workerUrl;
|
|
@@ -16,6 +18,7 @@ const snapshot = {
|
|
|
16
18
|
pendingBytes: 0,
|
|
17
19
|
maxPending: MAX_PENDING,
|
|
18
20
|
maxPendingBytes: MAX_PENDING_BYTES,
|
|
21
|
+
rejectionReasons: {},
|
|
19
22
|
};
|
|
20
23
|
const pending = new Map();
|
|
21
24
|
// Bound traversal as well as the structured clone sent to the worker. Never
|
|
@@ -29,12 +32,15 @@ function estimateCloneBytes(value) {
|
|
|
29
32
|
const seen = new Set();
|
|
30
33
|
let bytes = 0, nodes = 0;
|
|
31
34
|
while (stack.length) {
|
|
32
|
-
if (++nodes > 100_000
|
|
33
|
-
|
|
35
|
+
if (++nodes > 100_000) {
|
|
36
|
+
throw new Error("body_capture_traversal_limit");
|
|
37
|
+
}
|
|
38
|
+
if (bytes > MAX_ENTRY_BYTES) {
|
|
39
|
+
throw new Error("body_capture_entry_too_large");
|
|
34
40
|
}
|
|
35
41
|
const item = stack.pop();
|
|
36
42
|
if (typeof item === "string") {
|
|
37
|
-
bytes += item.length *
|
|
43
|
+
bytes += 16 + item.length * 2;
|
|
38
44
|
continue;
|
|
39
45
|
}
|
|
40
46
|
bytes += 16;
|
|
@@ -42,23 +48,26 @@ function estimateCloneBytes(value) {
|
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
if (seen.has(item)) {
|
|
45
|
-
|
|
51
|
+
throw new Error("body_capture_unsupported_value");
|
|
46
52
|
}
|
|
47
53
|
seen.add(item);
|
|
48
54
|
if (!Array.isArray(item) &&
|
|
49
55
|
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
50
56
|
Object.getPrototypeOf(item) !== null) {
|
|
51
|
-
|
|
57
|
+
throw new Error("body_capture_unsupported_value");
|
|
52
58
|
}
|
|
53
59
|
for (const key of Object.keys(item)) {
|
|
54
|
-
bytes += key.length *
|
|
60
|
+
bytes += 16 + key.length * 2;
|
|
55
61
|
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
56
62
|
if (!descriptor || descriptor.get || descriptor.set) {
|
|
57
|
-
|
|
63
|
+
throw new Error("body_capture_unsupported_value");
|
|
58
64
|
}
|
|
59
65
|
stack.push(descriptor.value);
|
|
60
|
-
if (stack.length > 100_000
|
|
61
|
-
|
|
66
|
+
if (stack.length > 100_000) {
|
|
67
|
+
throw new Error("body_capture_traversal_limit");
|
|
68
|
+
}
|
|
69
|
+
if (bytes > MAX_ENTRY_BYTES) {
|
|
70
|
+
throw new Error("body_capture_entry_too_large");
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -125,11 +134,21 @@ function getWorker() {
|
|
|
125
134
|
export async function captureProxyBody(entry, logDir, consume) {
|
|
126
135
|
snapshot.attempted += 1;
|
|
127
136
|
let bytes;
|
|
137
|
+
let admissionError;
|
|
128
138
|
try {
|
|
129
139
|
bytes = estimateCloneBytes(entry);
|
|
130
140
|
}
|
|
131
|
-
catch {
|
|
141
|
+
catch (error) {
|
|
132
142
|
bytes = Infinity;
|
|
143
|
+
admissionError =
|
|
144
|
+
error instanceof Error &&
|
|
145
|
+
[
|
|
146
|
+
"body_capture_entry_too_large",
|
|
147
|
+
"body_capture_traversal_limit",
|
|
148
|
+
"body_capture_unsupported_value",
|
|
149
|
+
].includes(error.message)
|
|
150
|
+
? error.message
|
|
151
|
+
: "body_capture_unsupported_value";
|
|
133
152
|
}
|
|
134
153
|
if (bytes > MAX_ENTRY_BYTES ||
|
|
135
154
|
snapshot.pending >= MAX_PENDING ||
|
|
@@ -137,11 +156,13 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
137
156
|
Date.now() < retryAfter) {
|
|
138
157
|
snapshot.rejected += 1;
|
|
139
158
|
const error = bytes > MAX_ENTRY_BYTES
|
|
140
|
-
? "
|
|
159
|
+
? (admissionError ?? "body_capture_entry_too_large")
|
|
141
160
|
: Date.now() < retryAfter
|
|
142
161
|
? "body_worker_backoff"
|
|
143
162
|
: "body_capture_queue_full";
|
|
144
163
|
snapshot.lastError = error;
|
|
164
|
+
snapshot.rejectionReasons[error] =
|
|
165
|
+
(snapshot.rejectionReasons[error] ?? 0) + 1;
|
|
145
166
|
return consume({ error, stored: { bodyWriteFailed: true } });
|
|
146
167
|
}
|
|
147
168
|
let current;
|
|
@@ -204,7 +225,7 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
204
225
|
* retained publication work.
|
|
205
226
|
*/
|
|
206
227
|
export function getBodyCaptureWorkerSnapshot() {
|
|
207
|
-
return { ...snapshot };
|
|
228
|
+
return { ...snapshot, rejectionReasons: { ...snapshot.rejectionReasons } };
|
|
208
229
|
}
|
|
209
230
|
/** Isolated tests point at a separately executed built worker. */
|
|
210
231
|
export const __bodyCaptureWorkerTestHooks = {
|
|
@@ -227,6 +248,7 @@ export const __bodyCaptureWorkerTestHooks = {
|
|
|
227
248
|
pending: 0,
|
|
228
249
|
pendingBytes: 0,
|
|
229
250
|
lastError: undefined,
|
|
251
|
+
rejectionReasons: {},
|
|
230
252
|
});
|
|
231
253
|
},
|
|
232
254
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import type { ProxyBodyChunkEmitter, ProxyBodyDeliveryResult } from "../types/index.js";
|
|
2
3
|
/** Explicit opt-in; configuration never silently falls back to file logging. */
|
|
3
4
|
export declare function isProxyOtelOnly(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Own a whole capture within byte/count bounds, then pace its chunks by actual
|
|
7
|
+
* export callbacks. SDK forceFlush alone does not await an automatic export
|
|
8
|
+
* already in flight. Serial publication prevents bursts from dropping tails.
|
|
9
|
+
*/
|
|
10
|
+
export declare function publishProxyOtelBody(captureId: string, body: string, emit: ProxyBodyChunkEmitter): Promise<ProxyBodyDeliveryResult>;
|
|
4
11
|
/** Initialize a log-only provider in every proxy process, including the supervisor. */
|
|
5
12
|
export declare function initializeProxyOtelLogs(role?: string): LoggerProvider | undefined;
|
|
6
13
|
/** Structured evidence without final-request dashboard fields on auxiliary events. */
|
|
@@ -13,6 +20,19 @@ export declare function getProxyOtelLogSnapshot(): {
|
|
|
13
20
|
initialized: boolean;
|
|
14
21
|
deliveryGuarantee: string;
|
|
15
22
|
invalidRecords: number;
|
|
23
|
+
bodyDelivery: {
|
|
24
|
+
attempted: number;
|
|
25
|
+
transportAcknowledged: number;
|
|
26
|
+
exportUnconfirmed: number;
|
|
27
|
+
rejected: number;
|
|
28
|
+
partial: number;
|
|
29
|
+
pending: number;
|
|
30
|
+
pendingBytes: number;
|
|
31
|
+
highWaterPending: number;
|
|
32
|
+
highWaterBytes: number;
|
|
33
|
+
maxPending: number;
|
|
34
|
+
maxPendingBytes: number;
|
|
35
|
+
};
|
|
16
36
|
queues: {
|
|
17
37
|
attempted: number;
|
|
18
38
|
submitted: number;
|
|
@@ -22,7 +42,8 @@ export declare function getProxyOtelLogSnapshot(): {
|
|
|
22
42
|
outstanding: number;
|
|
23
43
|
lastAcknowledgedAt: string | undefined;
|
|
24
44
|
lastFailureAt: string | undefined;
|
|
25
|
-
|
|
45
|
+
highWaterOutstanding: number;
|
|
46
|
+
kind: "metadata" | "bodies";
|
|
26
47
|
capacity: number;
|
|
27
48
|
}[];
|
|
28
49
|
};
|