@tonyclaw/agent-inspector 2.0.18 → 2.0.20

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 (73) hide show
  1. package/.output/cli.js +137 -8
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-CepU-dLW.js → CompareDrawer-CUqYtaH3.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CX0-R0nn.js +114 -0
  5. package/.output/public/assets/ReplayDialog-CPXNEqVg.js +1 -0
  6. package/.output/public/assets/{RequestAnatomy-C_C6ZU3T.js → RequestAnatomy-qkj8QgkY.js} +1 -1
  7. package/.output/public/assets/ResponseView-BPNLZpuw.js +1 -0
  8. package/.output/public/assets/StreamingChunkSequence-DGs-YFXI.js +1 -0
  9. package/.output/public/assets/_sessionId-CXtbrFTj.js +1 -0
  10. package/.output/public/assets/index-DrppF281.js +1 -0
  11. package/.output/public/assets/index-DynnYt7S.css +1 -0
  12. package/.output/public/assets/{main-BRXsrxtv.js → main-j3vS8rmP.js} +2 -2
  13. package/.output/server/_libs/lucide-react.mjs +141 -118
  14. package/.output/server/_libs/tanstack__react-virtual.mjs +13 -2
  15. package/.output/server/_libs/tanstack__virtual-core.mjs +24 -1
  16. package/.output/server/{_sessionId-B8Bbfs-v.mjs → _sessionId-Bc5Nohhe.mjs} +3 -2
  17. package/.output/server/_ssr/{CompareDrawer-TgGZLf1J.mjs → CompareDrawer-D0UewydM.mjs} +4 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-B_ppaNmQ.mjs → ProxyViewerContainer-BEUWt2eX.mjs} +351 -76
  19. package/.output/server/_ssr/{ReplayDialog-BXlajnMC.mjs → ReplayDialog-zaN-xMOw.mjs} +199 -17
  20. package/.output/server/_ssr/{RequestAnatomy-CCrx09-i.mjs → RequestAnatomy-BMomYged.mjs} +4 -3
  21. package/.output/server/_ssr/{ResponseView-BxtMJrov.mjs → ResponseView-DSoHCyAM.mjs} +4 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BlpdSPb8.mjs → StreamingChunkSequence-pt3pZOOL.mjs} +4 -3
  23. package/.output/server/_ssr/{index-BeTMNSJy.mjs → index-BCTKxcgV.mjs} +3 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{router-Ddl1tuU7.mjs → router-DpaDa5q5.mjs} +872 -232
  26. package/.output/server/{_tanstack-start-manifest_v-Bvdz_U0f.mjs → _tanstack-start-manifest_v-EsubwO0A.mjs} +1 -1
  27. package/.output/server/index.mjs +51 -51
  28. package/README.md +11 -109
  29. package/package.json +2 -1
  30. package/src/cli/doctor.ts +150 -5
  31. package/src/components/OnboardingBanner.tsx +13 -13
  32. package/src/components/ProxyViewer.tsx +55 -38
  33. package/src/components/ProxyViewerContainer.tsx +5 -0
  34. package/src/components/providers/SettingsDialog.tsx +113 -2
  35. package/src/components/proxy-viewer/ConversationGroup.tsx +20 -3
  36. package/src/components/proxy-viewer/ConversationGroupList.tsx +144 -0
  37. package/src/components/proxy-viewer/ConversationHeader.tsx +0 -2
  38. package/src/components/proxy-viewer/ReplayDialog.tsx +200 -13
  39. package/src/components/proxy-viewer/index.ts +1 -1
  40. package/src/components/ui/scroll-area.tsx +2 -2
  41. package/src/knowledge/openclawClient.ts +0 -8
  42. package/src/knowledge/types.ts +0 -2
  43. package/src/lib/export-logs.ts +72 -5
  44. package/src/lib/providerTestContract.ts +0 -1
  45. package/src/lib/useProviders.ts +1 -1
  46. package/src/mcp/server.ts +14 -6
  47. package/src/mcp/toolHandlers.ts +21 -4
  48. package/src/proxy/chunkStorage.ts +2 -4
  49. package/src/proxy/dataDir.ts +3 -3
  50. package/src/proxy/formats/anthropic/index.ts +0 -1
  51. package/src/proxy/formats/anthropic/schemas.ts +0 -2
  52. package/src/proxy/handler.ts +41 -29
  53. package/src/proxy/logFinalizer.ts +93 -11
  54. package/src/proxy/logger.ts +35 -10
  55. package/src/proxy/providers.ts +12 -5
  56. package/src/proxy/rawStreamCapture.ts +85 -0
  57. package/src/proxy/schemas.ts +1 -51
  58. package/src/proxy/sessionRuntime.ts +0 -9
  59. package/src/proxy/socketTracker.ts +15 -8
  60. package/src/proxy/store.ts +414 -13
  61. package/src/routes/api/logs.$id.replay.ts +66 -1
  62. package/src/routes/api/logs.stream.ts +11 -3
  63. package/src/routes/api/logs.ts +51 -17
  64. package/src/routes/api/providers.$providerId.ts +10 -3
  65. package/src/routes/api/providers.ts +6 -3
  66. package/.output/public/assets/ProxyViewerContainer-BG7rc33d.js +0 -114
  67. package/.output/public/assets/ReplayDialog-DK6SPv8X.js +0 -1
  68. package/.output/public/assets/ResponseView-Bqpra4Zw.js +0 -1
  69. package/.output/public/assets/StreamingChunkSequence-CdPbpprq.js +0 -1
  70. package/.output/public/assets/_sessionId-CsNARHj5.js +0 -1
  71. package/.output/public/assets/index-37NfO0zx.js +0 -1
  72. package/.output/public/assets/index-D_nZj9Vt.css +0 -1
  73. package/src/components/ui/json-expansion-button.tsx +0 -56
@@ -23,6 +23,7 @@ import { stripClaudeCodeBillingHeader } from "./claudeCodeStrip";
23
23
  import { stripOpenAIOrphanToolMessages } from "./openaiOrphanToolStrip";
24
24
  import { buildFileLogEntry, type FinalizeLogJob } from "./logFinalizer";
25
25
  import { enqueueFinalizeLogJob } from "./sessionRuntime";
26
+ import { RawStreamCapture } from "./rawStreamCapture";
26
27
  import {
27
28
  buildUpstreamUrl,
28
29
  describeApiRoute,
@@ -138,27 +139,50 @@ function handleStreamingResponse(
138
139
  log.streaming = true;
139
140
  log.responseStatus = upstreamRes.status;
140
141
 
141
- const chunks: string[] = [];
142
+ const capture = new RawStreamCapture();
142
143
  const decoder = new TextDecoder();
144
+ let finalizationScheduled = false;
145
+
146
+ const scheduleStreamingFinalization = (type: "streaming" | "stream-abort"): void => {
147
+ if (finalizationScheduled) return;
148
+ finalizationScheduled = true;
149
+
150
+ const elapsedMs = Date.now() - startTime;
151
+ const rawStream = capture.snapshot();
152
+ switch (type) {
153
+ case "streaming":
154
+ scheduleLogFinalization({
155
+ type: "streaming",
156
+ log,
157
+ upstreamUrl,
158
+ elapsedMs,
159
+ responseStatus: upstreamRes.status,
160
+ rawStream,
161
+ collectStreamingChunks,
162
+ });
163
+ return;
164
+ case "stream-abort":
165
+ scheduleLogFinalization({
166
+ type: "stream-abort",
167
+ log,
168
+ upstreamUrl,
169
+ elapsedMs,
170
+ rawStream,
171
+ hasChunks: capture.hasChunks,
172
+ collectStreamingChunks,
173
+ });
174
+ return;
175
+ }
176
+ };
143
177
 
144
178
  const transform = new TransformStream<Uint8Array, Uint8Array>({
145
- transform(chunk, controller) {
179
+ async transform(chunk, controller) {
146
180
  controller.enqueue(chunk);
147
- chunks.push(decoder.decode(chunk, { stream: true }));
181
+ await capture.append(decoder.decode(chunk, { stream: true }));
148
182
  },
149
- flush() {
150
- const full = chunks.join("");
151
- const elapsedMs = Date.now() - startTime;
152
-
153
- scheduleLogFinalization({
154
- type: "streaming",
155
- log,
156
- upstreamUrl,
157
- elapsedMs,
158
- responseStatus: upstreamRes.status,
159
- rawStream: full,
160
- collectStreamingChunks,
161
- });
183
+ async flush() {
184
+ await capture.append(decoder.decode());
185
+ scheduleStreamingFinalization("streaming");
162
186
  },
163
187
  });
164
188
 
@@ -171,19 +195,7 @@ function handleStreamingResponse(
171
195
  req.signal?.addEventListener("abort", () => {
172
196
  if (log.responseText === null) {
173
197
  logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
174
- const elapsedMs = Date.now() - startTime;
175
- const full = chunks.join("");
176
- const hasChunks = chunks.length > 0;
177
-
178
- scheduleLogFinalization({
179
- type: "stream-abort",
180
- log,
181
- upstreamUrl,
182
- elapsedMs,
183
- rawStream: full,
184
- hasChunks,
185
- collectStreamingChunks,
186
- });
198
+ scheduleStreamingFinalization("stream-abort");
187
199
  }
188
200
  });
189
201
 
@@ -1,7 +1,9 @@
1
+ import { readFileSync, rmSync } from "node:fs";
1
2
  import { Worker } from "node:worker_threads";
2
3
  import { writeChunks } from "./chunkStorage";
3
4
  import { formatForPath } from "./formats";
4
5
  import { appendLogEntry, logger } from "./logger";
6
+ import type { RawStreamSource } from "./rawStreamCapture";
5
7
  import type { CapturedLog } from "./schemas";
6
8
  import { getSessionProcess, isSessionProcessAvailable } from "./sessionProcess";
7
9
  import { finalizeLogUpdate } from "./store";
@@ -21,13 +23,13 @@ export type FinalizeNonStreamingLogJob = BaseFinalizeLogJob & {
21
23
  export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
22
24
  type: "streaming";
23
25
  responseStatus: number;
24
- rawStream: string;
26
+ rawStream: string | RawStreamSource;
25
27
  collectStreamingChunks: boolean;
26
28
  };
27
29
 
28
30
  export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
29
31
  type: "stream-abort";
30
- rawStream: string;
32
+ rawStream: string | RawStreamSource;
31
33
  hasChunks: boolean;
32
34
  collectStreamingChunks: boolean;
33
35
  };
@@ -41,6 +43,7 @@ export type FinalizeLogResult = {
41
43
  log: CapturedLog;
42
44
  upstreamUrl: string;
43
45
  error: string | null;
46
+ cleanupRawStreamPath?: string | null;
44
47
  };
45
48
 
46
49
  export function buildFileLogEntry(log: CapturedLog, upstreamUrl: string): Record<string, unknown> {
@@ -98,6 +101,7 @@ function finalizeWithError(
98
101
  fallbackStatus: number,
99
102
  fallbackResponseText: string,
100
103
  err: unknown,
104
+ cleanupRawStreamPath: string | null = null,
101
105
  ): FinalizeLogResult {
102
106
  const message = errorMessage(err);
103
107
  logger.error(`[logFinalizer] Failed to finalize log #${log.id}:`, message);
@@ -105,7 +109,7 @@ function finalizeWithError(
105
109
  log.responseText = log.responseText ?? fallbackResponseText;
106
110
  log.elapsedMs = job.elapsedMs;
107
111
  log.error = message;
108
- return { log, upstreamUrl: job.upstreamUrl, error: message };
112
+ return { log, upstreamUrl: job.upstreamUrl, error: message, cleanupRawStreamPath };
109
113
  }
110
114
 
111
115
  function finalizeNonStreaming(
@@ -134,40 +138,100 @@ function finalizeNonStreaming(
134
138
  }
135
139
  }
136
140
 
141
+ function cleanupPathForRawStream(rawStream: string | RawStreamSource): string | null {
142
+ if (typeof rawStream === "string") return null;
143
+ switch (rawStream.type) {
144
+ case "memory":
145
+ return null;
146
+ case "file":
147
+ return rawStream.path;
148
+ }
149
+ }
150
+
151
+ function readRawStream(rawStream: string | RawStreamSource): {
152
+ rawStreamText: string;
153
+ cleanupRawStreamPath: string | null;
154
+ } {
155
+ if (typeof rawStream === "string") {
156
+ return { rawStreamText: rawStream, cleanupRawStreamPath: null };
157
+ }
158
+
159
+ switch (rawStream.type) {
160
+ case "memory":
161
+ return { rawStreamText: rawStream.rawStream, cleanupRawStreamPath: null };
162
+ case "file":
163
+ return {
164
+ rawStreamText: readFileSync(rawStream.path, "utf-8"),
165
+ cleanupRawStreamPath: rawStream.path,
166
+ };
167
+ }
168
+ }
169
+
137
170
  function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): FinalizeLogResult {
138
171
  const formatHandler = formatForPath(log.path);
139
172
  if (formatHandler === null) {
140
- return finalizeWithError(job, log, job.responseStatus, job.rawStream, "Unsupported format");
173
+ return finalizeWithError(
174
+ job,
175
+ log,
176
+ job.responseStatus,
177
+ "Streaming response unavailable",
178
+ "Unsupported format",
179
+ cleanupPathForRawStream(job.rawStream),
180
+ );
141
181
  }
142
182
 
183
+ let rawStreamText = "";
184
+ let cleanupRawStreamPath: string | null = null;
143
185
  try {
186
+ const raw = readRawStream(job.rawStream);
187
+ rawStreamText = raw.rawStreamText;
188
+ cleanupRawStreamPath = raw.cleanupRawStreamPath;
144
189
  log.elapsedMs = job.elapsedMs;
145
190
  log.responseStatus = job.responseStatus;
146
191
  log.responseText = formatHandler.extractStream(
147
- job.rawStream,
192
+ rawStreamText,
148
193
  log,
149
194
  log.model ?? undefined,
150
195
  job.collectStreamingChunks,
151
196
  );
152
197
  persistStreamingChunks(log);
153
198
 
154
- return { log, upstreamUrl: job.upstreamUrl, error: null };
199
+ return { log, upstreamUrl: job.upstreamUrl, error: null, cleanupRawStreamPath };
155
200
  } catch (err) {
156
- return finalizeWithError(job, log, job.responseStatus, job.rawStream, err);
201
+ return finalizeWithError(
202
+ job,
203
+ log,
204
+ job.responseStatus,
205
+ rawStreamText === "" ? "Streaming response unavailable" : rawStreamText,
206
+ err,
207
+ cleanupRawStreamPath ?? cleanupPathForRawStream(job.rawStream),
208
+ );
157
209
  }
158
210
  }
159
211
 
160
212
  function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog): FinalizeLogResult {
161
213
  const formatHandler = formatForPath(log.path);
162
214
  if (formatHandler === null && job.hasChunks) {
163
- return finalizeWithError(job, log, 499, "Client aborted", "Unsupported format");
215
+ return finalizeWithError(
216
+ job,
217
+ log,
218
+ 499,
219
+ "Client aborted",
220
+ "Unsupported format",
221
+ cleanupPathForRawStream(job.rawStream),
222
+ );
164
223
  }
165
224
 
225
+ let rawStreamText = "";
226
+ let cleanupRawStreamPath: string | null = null;
166
227
  try {
167
228
  log.elapsedMs = job.elapsedMs;
168
229
  if (job.hasChunks && formatHandler !== null) {
230
+ const raw = readRawStream(job.rawStream);
231
+ rawStreamText = raw.rawStreamText;
232
+ cleanupRawStreamPath = raw.cleanupRawStreamPath;
169
233
  log.responseText = formatHandler.extractStream(
170
- job.rawStream,
234
+ rawStreamText,
171
235
  log,
172
236
  log.model ?? undefined,
173
237
  job.collectStreamingChunks,
@@ -177,9 +241,16 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
177
241
  log.responseText = "Client aborted";
178
242
  }
179
243
 
180
- return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted" };
244
+ return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted", cleanupRawStreamPath };
181
245
  } catch (err) {
182
- return finalizeWithError(job, log, 499, "Client aborted", err);
246
+ return finalizeWithError(
247
+ job,
248
+ log,
249
+ 499,
250
+ rawStreamText === "" ? "Client aborted" : rawStreamText,
251
+ err,
252
+ cleanupRawStreamPath ?? cleanupPathForRawStream(job.rawStream),
253
+ );
183
254
  }
184
255
  }
185
256
 
@@ -199,6 +270,17 @@ export function buildFinalizeLogResult(job: FinalizeLogJob): FinalizeLogResult {
199
270
  export function commitFinalizeLogResult(result: FinalizeLogResult): void {
200
271
  appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: result.error });
201
272
  finalizeLogUpdate(result.log);
273
+ if (result.cleanupRawStreamPath !== undefined && result.cleanupRawStreamPath !== null) {
274
+ try {
275
+ rmSync(result.cleanupRawStreamPath, { force: true });
276
+ } catch (err) {
277
+ logger.warn(
278
+ "[logFinalizer] Failed to remove raw stream temp file:",
279
+ result.cleanupRawStreamPath,
280
+ errorMessage(err),
281
+ );
282
+ }
283
+ }
202
284
  }
203
285
 
204
286
  // ── Worker pool ────────────────────────────────────────────────
@@ -5,6 +5,10 @@ import { getDataDir } from "./dataDir";
5
5
 
6
6
  const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
7
7
 
8
+ function writeStderrFallback(message: string, error: unknown): void {
9
+ process.stderr.write(`${message} ${String(error)}\n`);
10
+ }
11
+
8
12
  export function resolveLogDir(): string {
9
13
  const logDirEnv = process.env["LOG_DIR"];
10
14
  if (logDirEnv !== undefined && logDirEnv !== "") {
@@ -51,8 +55,7 @@ export async function initLogger(): Promise<void> {
51
55
  }
52
56
  } catch (err) {
53
57
  // Log directory initialization errors but don't fail startup
54
- // eslint-disable-next-line no-console
55
- console.error("[logger] Failed to initialize log directory:", err);
58
+ writeStderrFallback("[logger] Failed to initialize log directory:", err);
56
59
  }
57
60
  }
58
61
 
@@ -74,8 +77,7 @@ async function writeAppLog(message: string): Promise<void> {
74
77
  await appendFile(logPath, `[${timestamp}] ${message}\n`, "utf-8");
75
78
  } catch (err) {
76
79
  // Log to stderr since file logging failed
77
- // eslint-disable-next-line no-console
78
- console.error(`[logger] Failed to write to ${logPath}:`, err);
80
+ writeStderrFallback(`[logger] Failed to write to ${logPath}:`, err);
79
81
  }
80
82
  }
81
83
 
@@ -103,6 +105,22 @@ export const logger = {
103
105
  const MAX_BUFFER_SIZE = 1000;
104
106
  let writeBuffer: string[] = [];
105
107
  let writeQueue: Promise<void> = Promise.resolve();
108
+ let logWriteLock: Promise<void> = Promise.resolve();
109
+
110
+ export async function runWithLogWriteLock<T>(fn: () => Promise<T>): Promise<T> {
111
+ const previousLock = logWriteLock;
112
+ let releaseLock = (): void => {};
113
+ logWriteLock = new Promise<void>((resolve) => {
114
+ releaseLock = resolve;
115
+ });
116
+
117
+ await previousLock;
118
+ try {
119
+ return await fn();
120
+ } finally {
121
+ releaseLock();
122
+ }
123
+ }
106
124
 
107
125
  function drainBuffer(): string {
108
126
  const toWrite = writeBuffer.join("");
@@ -110,7 +128,7 @@ function drainBuffer(): string {
110
128
  return toWrite;
111
129
  }
112
130
 
113
- async function flushWriteBuffer(): Promise<void> {
131
+ async function flushWriteBufferUnlocked(): Promise<void> {
114
132
  let toWrite: string;
115
133
  {
116
134
  toWrite = drainBuffer();
@@ -124,8 +142,7 @@ async function flushWriteBuffer(): Promise<void> {
124
142
  } catch (err) {
125
143
  // On failure, re-queue the data so it is not lost
126
144
  writeBuffer.unshift(toWrite);
127
- // eslint-disable-next-line no-console
128
- console.error("[logger] Failed to flush write buffer, re-queued:", err);
145
+ writeStderrFallback("[logger] Failed to flush write buffer, re-queued:", err);
129
146
  }
130
147
  }
131
148
 
@@ -133,9 +150,9 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
133
150
  const line = JSON.stringify(entry) + "\n";
134
151
  writeBuffer.push(line);
135
152
  if (writeBuffer.length >= MAX_BUFFER_SIZE) {
136
- writeQueue = writeQueue.then(() => flushWriteBuffer());
153
+ writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
137
154
  } else if (writeBuffer.length === 1) {
138
- writeQueue = writeQueue.then(() => flushWriteBuffer());
155
+ writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
139
156
  }
140
157
  }
141
158
 
@@ -146,10 +163,18 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
146
163
  export async function flushLogBuffer(): Promise<void> {
147
164
  await writeQueue;
148
165
  if (writeBuffer.length > 0) {
149
- await flushWriteBuffer();
166
+ await runWithLogWriteLock(flushWriteBufferUnlocked);
150
167
  }
151
168
  }
152
169
 
170
+ export async function runWithFlushedLogWriteLock<T>(fn: () => Promise<T>): Promise<T> {
171
+ await flushLogBuffer();
172
+ return await runWithLogWriteLock(async () => {
173
+ await flushWriteBufferUnlocked();
174
+ return await fn();
175
+ });
176
+ }
177
+
153
178
  // Ensure buffered logs are written before the process exits
154
179
  process.on("exit", () => {
155
180
  if (writeBuffer.length > 0) {
@@ -3,6 +3,7 @@ import Conf from "conf";
3
3
  import { randomUUID } from "crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
+ import { maskApiKey } from "../lib/mask";
6
7
  import { logger } from "./logger";
7
8
  import { getDataDir, hasExplicitDataDir } from "./dataDir";
8
9
  import {
@@ -380,6 +381,17 @@ export function getProvider(id: string): ProviderConfig | undefined {
380
381
  return providers.find((p) => p.id === id);
381
382
  }
382
383
 
384
+ export function redactProvider(provider: ProviderConfig): ProviderConfig {
385
+ return {
386
+ ...provider,
387
+ apiKey: maskApiKey(provider.apiKey),
388
+ };
389
+ }
390
+
391
+ export function redactProviders(providers: readonly ProviderConfig[]): ProviderConfig[] {
392
+ return providers.map(redactProvider);
393
+ }
394
+
383
395
  /**
384
396
  * Normalizes an API key by stripping "Bearer " prefix if present.
385
397
  */
@@ -630,11 +642,6 @@ export function importProviders(json: string): { imported: number; errors: strin
630
642
  return { imported, errors };
631
643
  }
632
644
 
633
- function maskApiKey(apiKey: string): string {
634
- if (apiKey.length <= 8) return "••••••••";
635
- return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
636
- }
637
-
638
645
  /**
639
646
  * Converts display model name to API usage name based on provider-specific rules.
640
647
  * Default behavior: returns model as-is.
@@ -0,0 +1,85 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, appendFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { getDataDir } from "./dataDir";
6
+
7
+ const DEFAULT_MEMORY_LIMIT_BYTES = 1024 * 1024;
8
+
9
+ export type RawStreamSource =
10
+ | {
11
+ type: "memory";
12
+ rawStream: string;
13
+ }
14
+ | {
15
+ type: "file";
16
+ path: string;
17
+ };
18
+
19
+ function resolveMemoryLimitBytes(): number {
20
+ const raw = process.env["AGENT_INSPECTOR_STREAM_MEMORY_LIMIT_BYTES"];
21
+ if (raw === undefined || raw === "") return DEFAULT_MEMORY_LIMIT_BYTES;
22
+ const parsed = Number(raw);
23
+ if (!Number.isInteger(parsed) || parsed < 0) return DEFAULT_MEMORY_LIMIT_BYTES;
24
+ return parsed;
25
+ }
26
+
27
+ function getRawStreamDir(): string {
28
+ return join(getDataDir(), "raw-streams");
29
+ }
30
+
31
+ function createRawStreamPath(): string {
32
+ return join(getRawStreamDir(), `${Date.now()}-${randomUUID()}.stream.tmp`);
33
+ }
34
+
35
+ export class RawStreamCapture {
36
+ private readonly memoryLimitBytes: number;
37
+ private chunks: string[] = [];
38
+ private byteLength = 0;
39
+ private filePath: string | null = null;
40
+ private sawChunk = false;
41
+
42
+ constructor(memoryLimitBytes: number = resolveMemoryLimitBytes()) {
43
+ this.memoryLimitBytes = memoryLimitBytes;
44
+ }
45
+
46
+ get hasChunks(): boolean {
47
+ return this.sawChunk;
48
+ }
49
+
50
+ async append(text: string): Promise<void> {
51
+ if (text === "") return;
52
+ this.sawChunk = true;
53
+
54
+ const nextBytes = Buffer.byteLength(text, "utf-8");
55
+ if (this.filePath === null && this.byteLength + nextBytes <= this.memoryLimitBytes) {
56
+ this.chunks.push(text);
57
+ this.byteLength += nextBytes;
58
+ return;
59
+ }
60
+
61
+ if (this.filePath === null) {
62
+ await this.spillToFile();
63
+ }
64
+
65
+ if (this.filePath !== null) {
66
+ await appendFile(this.filePath, text, "utf-8");
67
+ this.byteLength += nextBytes;
68
+ }
69
+ }
70
+
71
+ snapshot(): RawStreamSource {
72
+ if (this.filePath !== null) {
73
+ return { type: "file", path: this.filePath };
74
+ }
75
+ return { type: "memory", rawStream: this.chunks.join("") };
76
+ }
77
+
78
+ private async spillToFile(): Promise<void> {
79
+ const path = createRawStreamPath();
80
+ await mkdir(getRawStreamDir(), { recursive: true });
81
+ await writeFile(path, this.chunks.join(""), "utf-8");
82
+ this.filePath = path;
83
+ this.chunks = [];
84
+ }
85
+ }
@@ -8,8 +8,6 @@ export type { JsonValue };
8
8
 
9
9
  export type RequestFormat = "anthropic" | "openai" | "unknown";
10
10
 
11
- import { safeGetOwnProperty } from "../lib/objectUtils";
12
-
13
11
  /**
14
12
  * A single SSE event from a streaming response.
15
13
  */
@@ -50,6 +48,7 @@ export const CapturedLogSchema = z.object({
50
48
  headers: z.record(z.string(), z.string()).optional(),
51
49
  apiFormat: z.enum(["anthropic", "openai", "unknown"]).default("unknown"),
52
50
  isTest: z.boolean().optional().default(false),
51
+ replayOfLogId: z.number().nullable().optional(),
53
52
  providerName: z.string().nullable().optional(),
54
53
  clientPort: z.number().nullable().optional(),
55
54
  clientPid: z.number().nullable().optional(),
@@ -74,9 +73,6 @@ export type TokenUsage = {
74
73
  // Import types and schemas from formats
75
74
  // ============================================================
76
75
 
77
- // Import types and schemas from formats for internal use
78
- import { InspectorRequestSchema, type InspectorRequest } from "./formats/anthropic/schemas";
79
-
80
76
  // Re-export types and schemas from formats for backward compatibility
81
77
  export {
82
78
  InspectorRequestSchema,
@@ -84,7 +80,6 @@ export {
84
80
  SseEventSchema,
85
81
  } from "./formats/anthropic/schemas";
86
82
  export type {
87
- InspectorRequest,
88
83
  InspectorResponse,
89
84
  ResponseContentBlockType,
90
85
  } from "./formats/anthropic/schemas";
@@ -98,35 +93,6 @@ export {
98
93
  parseOpenAIResponse,
99
94
  } from "./formats/openai/schemas";
100
95
 
101
- // ============================================================
102
- // Utility functions
103
- // ============================================================
104
-
105
- function detectFormat(rawBody: string | null): RequestFormat {
106
- if (rawBody === null) return "unknown";
107
- try {
108
- const json: unknown = JSON.parse(rawBody);
109
- if (typeof json === "object" && json !== null && !Array.isArray(json)) {
110
- const hasModel = safeGetOwnProperty(json, "model") !== undefined;
111
- const hasMessages = safeGetOwnProperty(json, "messages") !== undefined;
112
- if (hasModel && hasMessages) {
113
- // Anthropic requests put `system` as a top-level key; OpenAI does not.
114
- // Both formats can have `tools`, so we check `system` as the discriminator.
115
- if (safeGetOwnProperty(json, "system") !== undefined) {
116
- return "anthropic";
117
- }
118
- const msgVal = safeGetOwnProperty(json, "messages");
119
- if (Array.isArray(msgVal)) {
120
- return "openai";
121
- }
122
- }
123
- }
124
- return "unknown";
125
- } catch {
126
- return "unknown";
127
- }
128
- }
129
-
130
96
  // Schema for model name in request body (used for routing)
131
97
  const RequestModelSchema = z.object({
132
98
  model: z.string(),
@@ -191,19 +157,3 @@ export function extractRequestMetadata(body: string | null, headers: Headers): R
191
157
  }
192
158
  return { model: null, sessionId: headerSessionId };
193
159
  }
194
-
195
- /**
196
- * Parse a raw request body into an InspectorRequest.
197
- * @deprecated Use FormatHandler.parseRequest() instead
198
- */
199
- export function parseRequest(rawBody: string | null): InspectorRequest | null {
200
- if (rawBody === null) return null;
201
- try {
202
- const json: unknown = JSON.parse(rawBody);
203
- const result = InspectorRequestSchema.safeParse(json);
204
- if (result.success) return result.data;
205
- return null;
206
- } catch {
207
- return null;
208
- }
209
- }
@@ -1,6 +1,5 @@
1
1
  import { logger } from "./logger";
2
2
  import { executeFinalizeLogJob, type FinalizeLogJob } from "./logFinalizer";
3
- import type { CapturedLog } from "./schemas";
4
3
  import {
5
4
  getLogSessionId,
6
5
  markSessionTaskFinished,
@@ -70,14 +69,6 @@ export function enqueueSessionTask<T>(
70
69
  return current;
71
70
  }
72
71
 
73
- export function enqueueLogTask<T>(
74
- log: CapturedLog,
75
- taskName: SessionTaskName,
76
- task: SessionTask<T>,
77
- ): Promise<T> {
78
- return enqueueSessionTask(getLogSessionId(log), taskName, task);
79
- }
80
-
81
72
  export function enqueueFinalizeLogJob(job: FinalizeLogJob): Promise<void> {
82
73
  return enqueueSessionTask(getLogSessionId(job.log), "finalize-log", () =>
83
74
  executeFinalizeLogJob(job),
@@ -1,5 +1,6 @@
1
1
  import { exec, execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { z } from "zod";
3
4
  import { logger } from "./logger";
4
5
 
5
6
  const execAsync = promisify(exec);
@@ -20,6 +21,12 @@ type CacheEntry = ClientInfo & {
20
21
  const cache = new Map<number, CacheEntry>();
21
22
  const CACHE_TTL_MS = 5 * 60 * 1000;
22
23
  const MAX_CACHE_SIZE = 200;
24
+ const RemotePortSchema = z.number().int().min(1).max(65_535);
25
+ const SocketSchema = z
26
+ .object({
27
+ remotePort: RemotePortSchema.nullish(),
28
+ })
29
+ .passthrough();
23
30
 
24
31
  // Deduplicate concurrent lookups for the same port
25
32
  const inflight = new Map<number, Promise<ClientInfo>>();
@@ -50,14 +57,14 @@ function setCache(port: number, info: ClientInfo): void {
50
57
  * Get the remote port from a Request's underlying socket.
51
58
  * Works with Bun, Node.js, and Nitro's event.node.req.
52
59
  */
53
- function extractRemotePort(request: Request): number | null {
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
55
- const socket = (request as any).socket;
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
57
- const remotePort = (socket as any)?.remotePort;
58
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
59
- if (remotePort !== undefined && remotePort !== null) return remotePort;
60
- return null;
60
+ export function extractRemotePort(request: Request): number | null {
61
+ const descriptor = Object.getOwnPropertyDescriptor(request, "socket");
62
+ if (descriptor === undefined || descriptor.get !== undefined) return null;
63
+ const parsed = SocketSchema.safeParse(descriptor.value);
64
+ if (!parsed.success) return null;
65
+ const remotePort = parsed.data.remotePort;
66
+ if (remotePort === undefined || remotePort === null) return null;
67
+ return remotePort;
61
68
  }
62
69
 
63
70
  /**