@juspay/neurolink 12.12.9 → 12.12.10

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 (34) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/browser/neurolink.min.js +394 -395
  3. package/dist/cli/commands/proxy.d.ts +4 -0
  4. package/dist/cli/commands/proxy.js +60 -18
  5. package/dist/cli/commands/proxyAnalyze.js +8 -1
  6. package/dist/proxy/bodyCaptureProcessing.d.ts +22 -0
  7. package/dist/proxy/bodyCaptureProcessing.js +219 -0
  8. package/dist/proxy/bodyCaptureWorker.d.ts +16 -0
  9. package/dist/proxy/bodyCaptureWorker.js +232 -0
  10. package/dist/proxy/bodyCaptureWorkerEntry.d.ts +1 -0
  11. package/dist/proxy/bodyCaptureWorkerEntry.js +34 -0
  12. package/dist/proxy/proxyAnalysis.js +159 -17
  13. package/dist/proxy/proxyLifecycle.d.ts +25 -0
  14. package/dist/proxy/proxyLifecycle.js +111 -5
  15. package/dist/proxy/proxyRequestKind.d.ts +2 -0
  16. package/dist/proxy/proxyRequestKind.js +6 -0
  17. package/dist/proxy/proxyRuntimeMetrics.d.ts +3 -0
  18. package/dist/proxy/proxyRuntimeMetrics.js +34 -0
  19. package/dist/proxy/requestLogger.d.ts +10 -6
  20. package/dist/proxy/requestLogger.js +99 -231
  21. package/dist/proxy/rollingProxyServer.js +15 -4
  22. package/dist/proxy/rollingWorkerProcess.d.ts +4 -0
  23. package/dist/proxy/rollingWorkerProcess.js +25 -8
  24. package/dist/proxy/rollingWorkerProtocol.d.ts +6 -0
  25. package/dist/proxy/rollingWorkerProtocol.js +12 -1
  26. package/dist/proxy/rollingWorkerSupervisor.d.ts +28 -0
  27. package/dist/proxy/rollingWorkerSupervisor.js +73 -25
  28. package/dist/proxy/socketWorkerRuntime.d.ts +5 -0
  29. package/dist/proxy/socketWorkerRuntime.js +19 -2
  30. package/dist/server/routes/codexProxyRoutes.js +39 -3
  31. package/dist/services/server/ai/observability/instrumentation.js +7 -1
  32. package/dist/types/cli.d.ts +1 -1
  33. package/dist/types/proxy.d.ts +69 -4
  34. package/package.json +1 -1
@@ -0,0 +1,232 @@
1
+ import { Worker } from "node:worker_threads";
2
+ const MAX_PENDING = 16;
3
+ const MAX_PENDING_BYTES = 32 * 1024 * 1024;
4
+ const MAX_ENTRY_BYTES = 8 * 1024 * 1024;
5
+ export const PROXY_BODY_CAPTURE_DEADLINE_MS = 20_000;
6
+ let worker;
7
+ let workerUrl;
8
+ let retryAfter = 0;
9
+ let nextId = 0;
10
+ const snapshot = {
11
+ attempted: 0,
12
+ completed: 0,
13
+ rejected: 0,
14
+ failed: 0,
15
+ pending: 0,
16
+ pendingBytes: 0,
17
+ maxPending: MAX_PENDING,
18
+ maxPendingBytes: MAX_PENDING_BYTES,
19
+ };
20
+ const pending = new Map();
21
+ // Bound traversal as well as the structured clone sent to the worker. Never
22
+ // invoke getters/toJSON or stringify a large body on the serving event loop.
23
+ /**
24
+ * Conservatively bound clone size and traversal work without invoking
25
+ * getters or serializers.
26
+ */
27
+ function estimateCloneBytes(value) {
28
+ const stack = [value];
29
+ const seen = new Set();
30
+ let bytes = 0, nodes = 0;
31
+ while (stack.length) {
32
+ if (++nodes > 100_000 || bytes > MAX_ENTRY_BYTES) {
33
+ return Infinity;
34
+ }
35
+ const item = stack.pop();
36
+ if (typeof item === "string") {
37
+ bytes += item.length * 3;
38
+ continue;
39
+ }
40
+ bytes += 16;
41
+ if (!item || typeof item !== "object") {
42
+ continue;
43
+ }
44
+ if (seen.has(item)) {
45
+ return Infinity;
46
+ }
47
+ seen.add(item);
48
+ if (!Array.isArray(item) &&
49
+ Object.getPrototypeOf(item) !== Object.prototype &&
50
+ Object.getPrototypeOf(item) !== null) {
51
+ return Infinity;
52
+ }
53
+ for (const key of Object.keys(item)) {
54
+ bytes += key.length * 3;
55
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
56
+ if (!descriptor || descriptor.get || descriptor.set) {
57
+ return Infinity;
58
+ }
59
+ stack.push(descriptor.value);
60
+ if (stack.length > 100_000 || bytes > MAX_ENTRY_BYTES) {
61
+ return Infinity;
62
+ }
63
+ }
64
+ }
65
+ return bytes;
66
+ }
67
+ /**
68
+ * Settle IPC ownership once while publication retains the capture memory
69
+ * lease.
70
+ */
71
+ function settle(id, result) {
72
+ const task = pending.get(id);
73
+ if (!task) {
74
+ return;
75
+ }
76
+ pending.delete(id);
77
+ clearTimeout(task.timer);
78
+ task.resolve(result);
79
+ if (!pending.size) {
80
+ worker?.unref();
81
+ }
82
+ }
83
+ /**
84
+ * Fail every pending capture explicitly and back off without moving bulk
85
+ * work to the caller.
86
+ */
87
+ function failWorker(current, reason) {
88
+ if (worker !== current) {
89
+ return;
90
+ }
91
+ worker = undefined;
92
+ retryAfter = Date.now() + 5_000;
93
+ for (const id of pending.keys()) {
94
+ settle(id, {
95
+ error: reason,
96
+ stored: { bodyWriteFailed: true },
97
+ });
98
+ }
99
+ void current.terminate().catch(() => undefined);
100
+ }
101
+ /**
102
+ * Lazily create the bounded worker; only admitted processing keeps it
103
+ * referenced.
104
+ */
105
+ function getWorker() {
106
+ if (worker) {
107
+ return worker;
108
+ }
109
+ const current = new Worker(workerUrl ?? new URL("./bodyCaptureWorkerEntry.js", import.meta.url), {
110
+ execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type")),
111
+ resourceLimits: { maxOldGenerationSizeMb: 128 },
112
+ });
113
+ worker = current;
114
+ current.on("message", (message) => {
115
+ if (worker === current) {
116
+ settle(message.id, message.result);
117
+ }
118
+ });
119
+ current.on("error", () => failWorker(current, "body_worker_error"));
120
+ current.on("exit", () => failWorker(current, "body_worker_exit"));
121
+ current.unref();
122
+ return current;
123
+ }
124
+ /** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
125
+ export async function captureProxyBody(entry, logDir, consume) {
126
+ snapshot.attempted += 1;
127
+ let bytes;
128
+ try {
129
+ bytes = estimateCloneBytes(entry);
130
+ }
131
+ catch {
132
+ bytes = Infinity;
133
+ }
134
+ if (bytes > MAX_ENTRY_BYTES ||
135
+ snapshot.pending >= MAX_PENDING ||
136
+ snapshot.pendingBytes + bytes > MAX_PENDING_BYTES ||
137
+ Date.now() < retryAfter) {
138
+ snapshot.rejected += 1;
139
+ const error = bytes > MAX_ENTRY_BYTES
140
+ ? "body_capture_too_large_or_non_json"
141
+ : Date.now() < retryAfter
142
+ ? "body_worker_backoff"
143
+ : "body_capture_queue_full";
144
+ snapshot.lastError = error;
145
+ return consume({ error, stored: { bodyWriteFailed: true } });
146
+ }
147
+ let current;
148
+ try {
149
+ current = getWorker();
150
+ }
151
+ catch {
152
+ snapshot.failed += 1;
153
+ retryAfter = Date.now() + 5_000;
154
+ return consume({
155
+ error: "body_worker_start_failed",
156
+ stored: { bodyWriteFailed: true },
157
+ });
158
+ }
159
+ const id = ++nextId;
160
+ return new Promise((resolve) => {
161
+ const timer = setTimeout(() => failWorker(current, "body_worker_timeout"), PROXY_BODY_CAPTURE_DEADLINE_MS);
162
+ timer.unref();
163
+ pending.set(id, {
164
+ timer,
165
+ resolve: (result) => {
166
+ // Keep the byte/count lease through index writes and OTLP publication,
167
+ // so completed worker results cannot form an unbounded parent backlog.
168
+ void consume(result)
169
+ .catch(() => {
170
+ result.error ??= "body_capture_publication_failed";
171
+ })
172
+ .finally(() => {
173
+ snapshot.pending -= 1;
174
+ snapshot.pendingBytes -= bytes;
175
+ if (result.error || result.stored.bodyWriteFailed) {
176
+ snapshot.failed += 1;
177
+ }
178
+ else {
179
+ snapshot.completed += 1;
180
+ }
181
+ if (result.error) {
182
+ snapshot.lastError = result.error;
183
+ }
184
+ resolve();
185
+ });
186
+ },
187
+ });
188
+ snapshot.pending += 1;
189
+ snapshot.pendingBytes += bytes;
190
+ current.ref();
191
+ try {
192
+ current.postMessage({ id, entry, logDir, queuedAt: Date.now() });
193
+ }
194
+ catch {
195
+ settle(id, {
196
+ error: "body_capture_clone_failed",
197
+ stored: { bodyWriteFailed: true },
198
+ });
199
+ }
200
+ });
201
+ }
202
+ /**
203
+ * Return independent counters for processing, rejection, failure, and
204
+ * retained publication work.
205
+ */
206
+ export function getBodyCaptureWorkerSnapshot() {
207
+ return { ...snapshot };
208
+ }
209
+ /** Isolated tests point at a separately executed built worker. */
210
+ export const __bodyCaptureWorkerTestHooks = {
211
+ /**
212
+ * Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
213
+ */
214
+ async reset(url) {
215
+ if (worker) {
216
+ const current = worker;
217
+ failWorker(current, "body_worker_test_reset");
218
+ await current.terminate();
219
+ }
220
+ workerUrl = url;
221
+ retryAfter = 0;
222
+ Object.assign(snapshot, {
223
+ attempted: 0,
224
+ completed: 0,
225
+ rejected: 0,
226
+ failed: 0,
227
+ pending: 0,
228
+ pendingBytes: 0,
229
+ lastError: undefined,
230
+ });
231
+ },
232
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { performance } from "node:perf_hooks";
3
+ import { processProxyBodyCapture } from "./bodyCaptureProcessing.js";
4
+ // Sequential processing bounds serialization/compression memory. The parent
5
+ // bounds both the queued record count and clone bytes, including in-flight work.
6
+ let tail = Promise.resolve();
7
+ parentPort?.on("message", (message) => {
8
+ tail = tail.then(async () => {
9
+ const started = performance.now();
10
+ const queueWaitMs = Math.max(0, Date.now() - message.queuedAt);
11
+ try {
12
+ const result = await processProxyBodyCapture(message.entry, message.logDir);
13
+ parentPort?.postMessage({
14
+ id: message.id,
15
+ result: {
16
+ ...result,
17
+ queueWaitMs,
18
+ processingMs: performance.now() - started,
19
+ },
20
+ });
21
+ }
22
+ catch {
23
+ parentPort?.postMessage({
24
+ id: message.id,
25
+ result: {
26
+ error: "body_capture_processing_failed",
27
+ stored: { bodyWriteFailed: true },
28
+ queueWaitMs,
29
+ processingMs: performance.now() - started,
30
+ },
31
+ });
32
+ }
33
+ });
34
+ });
@@ -1,17 +1,20 @@
1
1
  import { createReadStream } from "node:fs";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { isProxyAuxiliaryRequest } from "./proxyRequestKind.js";
3
4
  import { lstat, readdir, realpath, stat } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
6
  import { createInterface } from "node:readline";
6
7
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
7
8
  import { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES, } from "./routingEvidence.js";
8
9
  import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
9
- const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
10
+ const LIFECYCLE_FILE_PATTERN = /^proxy-(?:lifecycle|supervisor)-\d{4}-\d{2}-\d{2}\.jsonl$/;
10
11
  const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
11
12
  const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
12
13
  const DEBUG_FILE_PATTERN = /^proxy-debug-\d{4}-\d{2}-\d{2}\.jsonl$/;
13
14
  const ARTIFACT_STAT_CONCURRENCY = 64;
14
15
  const LIFECYCLE_EVENTS = new Set([
16
+ "runtime_sample",
17
+ "supervisor_event",
15
18
  "request_accepted",
16
19
  "response_headers",
17
20
  "response_first_chunk",
@@ -585,6 +588,7 @@ export async function analyzeProxyLogs(options) {
585
588
  let malformedLines = 0;
586
589
  let unsupportedLifecycleLines = 0;
587
590
  const accepted = new Set();
591
+ const auxiliaryRequests = new Set();
588
592
  const headers = new Set();
589
593
  const firstChunks = new Set();
590
594
  const terminal = new Set();
@@ -595,18 +599,36 @@ export async function analyzeProxyLogs(options) {
595
599
  const firstChunkLatencyByRequest = new Map();
596
600
  const terminalLatencyByRequest = new Map();
597
601
  const sequences = new Map();
602
+ const selectedSequenceRanges = new Map();
598
603
  const seenLifecycleEvents = new Map();
599
604
  let conflictingLifecycleDuplicates = 0;
600
605
  const conflictedRequests = new Set();
601
606
  const terminalRecords = new Map();
607
+ const acceptedWorkers = new Map();
608
+ const admittedWorkerIds = new Set();
609
+ const conflictedWorkerExits = new Set();
610
+ const conflictingIdentities = new Set();
611
+ const runtimeRecords = new Map();
612
+ const workerExits = new Map();
613
+ const runtime = {
614
+ samples: 0,
615
+ maxEventLoopDelayMs: null,
616
+ maxRssBytes: null,
617
+ maxCpuPercentOneCore: null,
618
+ maxHostLoad1m: null,
619
+ };
602
620
  for (const filePath of lifecycleFiles) {
603
621
  linesRead += await readJsonLines(filePath, (record) => {
604
- const timestamp = observeTimestamp("lifecycle", record);
622
+ const operational = record.event === "runtime_sample" ||
623
+ record.event === "supervisor_event";
624
+ const operationalTimestamp = Date.parse(String(record.timestamp));
625
+ const timestamp = operational
626
+ ? Number.isFinite(operationalTimestamp)
627
+ ? operationalTimestamp
628
+ : null
629
+ : observeTimestamp("lifecycle", record);
605
630
  const requestId = stringValue(record.requestId);
606
- if (timestamp === null ||
607
- !requestId ||
608
- (!accepted.has(requestId) &&
609
- (timestamp < sinceMs || timestamp > untilMs))) {
631
+ if (timestamp === null || !requestId) {
610
632
  return;
611
633
  }
612
634
  const event = stringValue(record.event);
@@ -614,7 +636,10 @@ export async function analyzeProxyLogs(options) {
614
636
  !event ||
615
637
  !LIFECYCLE_EVENTS.has(event) ||
616
638
  !requestId) {
617
- unsupportedLifecycleLines += 1;
639
+ if (accepted.has(requestId) ||
640
+ (timestamp >= sinceMs && timestamp <= untilMs)) {
641
+ unsupportedLifecycleLines += 1;
642
+ }
618
643
  return;
619
644
  }
620
645
  const processId = stringValue(record.processInstanceId);
@@ -623,11 +648,40 @@ export async function analyzeProxyLogs(options) {
623
648
  const values = sequences.get(processId) ?? [];
624
649
  values.push(sequence);
625
650
  sequences.set(processId, values);
651
+ }
652
+ const details = record.supervisorEvent && typeof record.supervisorEvent === "object"
653
+ ? record.supervisorEvent
654
+ : undefined;
655
+ const exitedWorkerId = stringValue(details?.workerProcessInstanceId);
656
+ const relatedExit = event === "supervisor_event" &&
657
+ details?.type === "worker_exit" &&
658
+ exitedWorkerId &&
659
+ admittedWorkerIds.has(exitedWorkerId);
660
+ // Audit intervening sequences before selecting the request cohort.
661
+ if (!accepted.has(requestId) &&
662
+ !relatedExit &&
663
+ (timestamp < sinceMs || timestamp > untilMs)) {
664
+ return;
665
+ }
666
+ if (processId && sequence !== null && Number.isInteger(sequence)) {
667
+ const range = selectedSequenceRanges.get(processId);
668
+ selectedSequenceRanges.set(processId, {
669
+ min: Math.min(range?.min ?? sequence, sequence),
670
+ max: Math.max(range?.max ?? sequence, sequence),
671
+ });
626
672
  const identity = `${processId}:${sequence}`;
627
673
  const previous = seenLifecycleEvents.get(identity);
628
674
  if (previous) {
629
675
  if (!isDeepStrictEqual(previous, record)) {
630
676
  conflictingLifecycleDuplicates += 1;
677
+ conflictingIdentities.add(identity);
678
+ for (const copy of [previous, record]) {
679
+ const detail = copy.supervisorEvent;
680
+ const workerId = stringValue(detail?.workerProcessInstanceId);
681
+ if (workerId) {
682
+ conflictedWorkerExits.add(workerId);
683
+ }
684
+ }
631
685
  conflictedRequests.add(requestId);
632
686
  const previousRequestId = stringValue(previous.requestId);
633
687
  if (previousRequestId) {
@@ -638,9 +692,36 @@ export async function analyzeProxyLogs(options) {
638
692
  }
639
693
  seenLifecycleEvents.set(identity, record);
640
694
  }
695
+ if (event === "runtime_sample") {
696
+ if (record.runtimeSample &&
697
+ typeof record.runtimeSample === "object" &&
698
+ processId &&
699
+ sequence !== null) {
700
+ runtimeRecords.set(`${processId}:${sequence}`, record.runtimeSample);
701
+ }
702
+ return;
703
+ }
704
+ if (event === "supervisor_event") {
705
+ if (details?.type === "worker_exit" && exitedWorkerId) {
706
+ const exit = { ...details, at: record.timestamp };
707
+ const previous = workerExits.get(exitedWorkerId);
708
+ if (previous && !isDeepStrictEqual(previous, exit)) {
709
+ conflictedWorkerExits.add(exitedWorkerId);
710
+ }
711
+ workerExits.set(exitedWorkerId, exit);
712
+ }
713
+ return;
714
+ }
715
+ if (isProxyAuxiliaryRequest(stringValue(record.method) ?? "", stringValue(record.path) ?? "")) {
716
+ auxiliaryRequests.add(requestId);
717
+ }
641
718
  const elapsed = finiteNumber(record.elapsedMs);
642
719
  if (event === "request_accepted") {
643
720
  accepted.add(requestId);
721
+ if (processId) {
722
+ acceptedWorkers.set(requestId, processId);
723
+ admittedWorkerIds.add(processId);
724
+ }
644
725
  }
645
726
  else if (event === "response_headers") {
646
727
  if (headers.has(requestId)) {
@@ -683,6 +764,25 @@ export async function analyzeProxyLogs(options) {
683
764
  malformedLines += 1;
684
765
  });
685
766
  }
767
+ for (const [identity, sample] of runtimeRecords) {
768
+ if (conflictingIdentities.has(identity)) {
769
+ continue;
770
+ }
771
+ runtime.samples += 1;
772
+ const mapping = {
773
+ maxEventLoopDelayMs: "eventLoopDelayMaxMs",
774
+ maxRssBytes: "rssBytes",
775
+ maxCpuPercentOneCore: "cpuPercentOneCore",
776
+ maxHostLoad1m: "hostLoad1m",
777
+ };
778
+ for (const [key, source] of Object.entries(mapping)) {
779
+ const value = finiteNumber(sample[source]);
780
+ if (value !== null && value >= 0) {
781
+ const target = key;
782
+ runtime[target] = Math.max(runtime[target] ?? value, value);
783
+ }
784
+ }
785
+ }
686
786
  // Contradictory copies are not reliable latency samples. Keep their data
687
787
  // quality count, but do not choose one timing arbitrarily.
688
788
  const verifiedLatencies = (values) => [...values]
@@ -693,7 +793,12 @@ export async function analyzeProxyLogs(options) {
693
793
  const terminalLatency = verifiedLatencies(terminalLatencyByRequest);
694
794
  let lifecycleSequenceGaps = 0;
695
795
  let lifecycleSequenceDuplicates = 0;
696
- for (const values of sequences.values()) {
796
+ for (const [processId, allValues] of sequences) {
797
+ const range = selectedSequenceRanges.get(processId);
798
+ if (!range) {
799
+ continue;
800
+ }
801
+ const values = allValues.filter((value) => value >= range.min && value <= range.max);
697
802
  values.sort((a, b) => a - b);
698
803
  for (let index = 1; index < values.length; index += 1) {
699
804
  const difference = values[index] - values[index - 1];
@@ -918,6 +1023,9 @@ export async function analyzeProxyLogs(options) {
918
1023
  for (const [requestId, record] of terminalRecords) {
919
1024
  const final = finalRequests.get(requestId);
920
1025
  const recorded = stringValue(record.terminalOutcome) ?? "unknown";
1026
+ const auxiliaryTransport = auxiliaryRequests.has(requestId)
1027
+ ? (stringValue(record.transportOutcome) ?? recorded)
1028
+ : null;
921
1029
  const resolved = final
922
1030
  ? final.status === 499 || final.errorType === "client_cancelled"
923
1031
  ? "client_cancelled"
@@ -927,11 +1035,20 @@ export async function analyzeProxyLogs(options) {
927
1035
  : final.status >= 400 || final.errorType
928
1036
  ? "handler_error"
929
1037
  : "completed"
930
- : conflictedRequests.has(requestId) ||
931
- recorded === "completed" ||
932
- recorded === "bodyless"
933
- ? "unknown"
934
- : recorded;
1038
+ : auxiliaryTransport && !conflictedRequests.has(requestId)
1039
+ ? auxiliaryTransport === "completed" ||
1040
+ auxiliaryTransport === "bodyless"
1041
+ ? finiteNumber(record.responseStatus) === null
1042
+ ? "unknown"
1043
+ : Number(record.responseStatus) >= 400
1044
+ ? "handler_error"
1045
+ : auxiliaryTransport
1046
+ : auxiliaryTransport
1047
+ : conflictedRequests.has(requestId) ||
1048
+ recorded === "completed" ||
1049
+ recorded === "bodyless"
1050
+ ? "unknown"
1051
+ : recorded;
935
1052
  if (final &&
936
1053
  recorded !== resolved &&
937
1054
  !(recorded === "bodyless" && resolved === "completed")) {
@@ -1038,10 +1155,8 @@ export async function analyzeProxyLogs(options) {
1038
1155
  conflictingLifecycleDuplicates,
1039
1156
  duplicateAttempts,
1040
1157
  finalOutcomeConflicts,
1041
- acceptedWithoutFinal: [...accepted].filter((id) => !finalRequests.has(id))
1042
- .length,
1043
- terminalWithoutFinal: [...terminal].filter((id) => !finalRequests.has(id))
1044
- .length,
1158
+ acceptedWithoutFinal: [...accepted].filter((id) => !finalRequests.has(id) && !auxiliaryRequests.has(id)).length,
1159
+ terminalWithoutFinal: [...terminal].filter((id) => !finalRequests.has(id) && !auxiliaryRequests.has(id)).length,
1045
1160
  streams: Object.fromEntries(Object.entries(observedRanges).map(([stream, range]) => [
1046
1161
  stream,
1047
1162
  {
@@ -1066,8 +1181,35 @@ export async function analyzeProxyLogs(options) {
1066
1181
  absent: absentRoutingDecisions,
1067
1182
  },
1068
1183
  },
1184
+ runtime,
1069
1185
  lifecycle: {
1186
+ unconfirmedAtWorkerExit: [...accepted].flatMap((requestId) => {
1187
+ const workerProcessInstanceId = acceptedWorkers.get(requestId);
1188
+ const exit = workerProcessInstanceId
1189
+ ? workerExits.get(workerProcessInstanceId)
1190
+ : undefined;
1191
+ if (!exit ||
1192
+ !workerProcessInstanceId ||
1193
+ conflictedWorkerExits.has(workerProcessInstanceId) ||
1194
+ terminal.has(requestId) ||
1195
+ conflictedRequests.has(requestId)) {
1196
+ return [];
1197
+ }
1198
+ return [
1199
+ {
1200
+ requestId,
1201
+ workerProcessInstanceId,
1202
+ at: String(exit.at),
1203
+ workerExitCode: finiteNumber(exit.workerExitCode),
1204
+ workerExitSignal: stringValue(exit.workerExitSignal),
1205
+ // A provider final cannot prove the client received the entire body.
1206
+ providerFinalRecorded: finalRequests.has(requestId),
1207
+ },
1208
+ ];
1209
+ }),
1070
1210
  accepted: accepted.size,
1211
+ auxiliaryRequests: [...accepted].filter((id) => auxiliaryRequests.has(id))
1212
+ .length,
1071
1213
  headers: headers.size,
1072
1214
  firstChunks: firstChunks.size,
1073
1215
  terminal: terminal.size,
@@ -1,13 +1,38 @@
1
+ import { chmodSync } from "node:fs";
1
2
  import { appendFile } from "node:fs/promises";
2
3
  import type { ProxyLifecycleEventInput, ProxyLifecycleLoggerOptions, ProxyLifecycleLoggerSnapshot } from "../types/index.js";
3
4
  export declare function hashProxyLifecycleSessionId(sessionId: string | undefined): string | undefined;
5
+ /**
6
+ * Configure private journal storage while retaining required admission
7
+ * when initialization fails.
8
+ */
4
9
  export declare function configureProxyLifecycleLogger(options: ProxyLifecycleLoggerOptions): void;
5
10
  /** Enqueue fixed-size lifecycle metadata without awaiting filesystem work. */
6
11
  export declare function logProxyLifecycleEvent(input: ProxyLifecycleEventInput): void;
12
+ /** Confirm admission before upstream dispatch, without waiting for later traffic.
13
+ * Disabled logging is explicit; an enabled but unhealthy sink refuses dispatch.
14
+ * A timeout never retries an ambiguous provider request or the pending append.
15
+ */
16
+ export declare function persistProxyLifecycleAcceptance(input: Omit<ProxyLifecycleEventInput, "event">, timeoutMs?: number): Promise<void>;
7
17
  export declare function flushProxyLifecycleEvents(timeoutMs?: number): Promise<void>;
18
+ /**
19
+ * Expose journal accounting, process identity, and outstanding writes
20
+ * without changing them.
21
+ */
8
22
  export declare function getProxyLifecycleLoggerSnapshot(): ProxyLifecycleLoggerSnapshot;
23
+ /**
24
+ * Reset timers, accounting, and injected I/O after isolated tests have
25
+ * drained their work.
26
+ */
9
27
  export declare function resetProxyLifecycleLoggerForTests(): void;
10
28
  /** Isolated failure injection for lifecycle durability tests. */
11
29
  export declare const __proxyLifecycleTestHooks: {
30
+ /**
31
+ * Inject directory-hardening failures without altering real filesystem permissions.
32
+ */
33
+ setChmodForTests(chmod: typeof chmodSync): void;
34
+ /**
35
+ * Inject controlled append outcomes while preserving the production admission and queue paths.
36
+ */
12
37
  setAppendFileForTests(append: typeof appendFile): void;
13
38
  };