@tonyclaw/agent-inspector 2.0.22 → 2.0.24
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-NEdpe1wl.js → CompareDrawer-DrY-gAzy.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CiLvRNie.js +115 -0
- package/.output/public/assets/{ReplayDialog-DijzJwaQ.js → ReplayDialog-CFyw6Vu4.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CYbosGYy.js → RequestAnatomy-DHGJ9py7.js} +1 -1
- package/.output/public/assets/{ResponseView-B3dPM63X.js → ResponseView-CB_3yc08.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-DYDx3xRY.js → StreamingChunkSequence-CqhHwgDn.js} +1 -1
- package/.output/public/assets/_sessionId-CgG9CQ9r.js +1 -0
- package/.output/public/assets/index-CViEXkqa.js +1 -0
- package/.output/public/assets/{main-N-sjHJUM.js → main-CVw7-JyE.js} +2 -2
- package/.output/server/{_sessionId-BR46y_bO.mjs → _sessionId-V33sBWBC.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-DDxpmdPs.mjs → CompareDrawer-C_Fk9joM.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-B1zAefzz.mjs → ProxyViewerContainer-CyFKksQa.mjs} +75 -32
- package/.output/server/_ssr/{ReplayDialog-DleMFvMx.mjs → ReplayDialog-BpQHN7O2.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-B-P9tC3D.mjs → RequestAnatomy-CuhmKAma.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DWfGn0i3.mjs → ResponseView-CO6LyBmj.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-C8kFF6xJ.mjs → StreamingChunkSequence-DLeVetNR.mjs} +2 -2
- package/.output/server/_ssr/{index-DzENi2RO.mjs → index-DWUKl9Bf.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Dy1VEjxy.mjs → router-BrK_DcTS.mjs} +337 -86
- package/.output/server/{_tanstack-start-manifest_v-CyzuAD8A.mjs → _tanstack-start-manifest_v-BjW2XeWE.mjs} +1 -1
- package/.output/server/index.mjs +57 -57
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +11 -1
- package/src/components/ProxyViewerContainer.tsx +21 -15
- package/src/lib/export-logs.ts +54 -11
- package/src/lib/providerTestPrompt.ts +4 -4
- package/src/mcp/toolHandlers.ts +58 -8
- package/src/proxy/logFinalizer.ts +79 -1
- package/src/proxy/logIndex.ts +6 -1
- package/src/proxy/schemas.ts +3 -0
- package/src/proxy/store.ts +275 -135
- package/src/routes/api/logs.stream.ts +6 -3
- package/src/routes/api/logs.ts +4 -1
- package/.output/public/assets/ProxyViewerContainer-Dwhi4Q1c.js +0 -115
- package/.output/public/assets/_sessionId-b1KDnByv.js +0 -1
- package/.output/public/assets/index-CUyRKgII.js +0 -1
package/src/proxy/store.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
createReadStream,
|
|
4
|
+
createWriteStream,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
type WriteStream,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { open, unlink, rename } from "node:fs/promises";
|
|
3
10
|
import { readdir as readdirCallback } from "node:fs/promises";
|
|
4
11
|
import { createInterface } from "node:readline";
|
|
5
12
|
import { Buffer } from "node:buffer";
|
|
@@ -18,7 +25,9 @@ import {
|
|
|
18
25
|
flushIndex,
|
|
19
26
|
getNextLogId,
|
|
20
27
|
getCurrentLogFile,
|
|
28
|
+
listIndexEntries,
|
|
21
29
|
rebuildIndex,
|
|
30
|
+
type LogIndexEntry,
|
|
22
31
|
} from "./logIndex";
|
|
23
32
|
import { writeChunks } from "./chunkStorage";
|
|
24
33
|
import type { CapturedLog } from "./schemas";
|
|
@@ -68,6 +77,7 @@ export type ListLogsPageOptions = {
|
|
|
68
77
|
model?: string;
|
|
69
78
|
offset: number;
|
|
70
79
|
limit: number;
|
|
80
|
+
includeBodies?: boolean;
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
export type ListLogsPageResult = {
|
|
@@ -109,6 +119,29 @@ function removeFromCache(id: number): void {
|
|
|
109
119
|
memoryCache.delete(id);
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
function textByteLength(value: string | null): number | null {
|
|
123
|
+
if (value === null) return null;
|
|
124
|
+
return Buffer.byteLength(value, "utf8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function compactLogForList(log: CapturedLog): CapturedLog {
|
|
128
|
+
return {
|
|
129
|
+
...log,
|
|
130
|
+
rawRequestBody: null,
|
|
131
|
+
responseText: null,
|
|
132
|
+
rawHeaders: undefined,
|
|
133
|
+
headers: undefined,
|
|
134
|
+
streamingChunks: undefined,
|
|
135
|
+
rawRequestBodyBytes: textByteLength(log.rawRequestBody),
|
|
136
|
+
responseTextBytes: textByteLength(log.responseText),
|
|
137
|
+
bodyContentMode: "compact",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function prepareLogForList(log: CapturedLog, includeBodies: boolean): CapturedLog {
|
|
142
|
+
return includeBodies ? log : compactLogForList(log);
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
/**
|
|
113
146
|
* Add a test log entry to the in-memory store and persistent log file.
|
|
114
147
|
* This is used by the provider test endpoint to seed the provider-test session.
|
|
@@ -273,33 +306,7 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
|
|
|
273
306
|
return null;
|
|
274
307
|
}
|
|
275
308
|
|
|
276
|
-
|
|
277
|
-
const content = readFileSync(filePath, "utf-8");
|
|
278
|
-
const lines = content.split("\n");
|
|
279
|
-
|
|
280
|
-
let lastMatch: CapturedLog | null = null;
|
|
281
|
-
for (const line of lines) {
|
|
282
|
-
if (line.trim() === "") continue;
|
|
283
|
-
try {
|
|
284
|
-
const parsed: unknown = JSON.parse(line);
|
|
285
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
286
|
-
const desc = Object.getOwnPropertyDescriptor(parsed, "id");
|
|
287
|
-
if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
|
|
288
|
-
const result = CapturedLogSchema.safeParse(parsed);
|
|
289
|
-
if (result.success) {
|
|
290
|
-
lastMatch = normalizeLogSession(result.data);
|
|
291
|
-
if (result.data.responseStatus !== null) {
|
|
292
|
-
addToCache(lastMatch);
|
|
293
|
-
observeSessionLog(lastMatch);
|
|
294
|
-
return lastMatch;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
} catch {
|
|
300
|
-
// Skip malformed lines
|
|
301
|
-
}
|
|
302
|
-
}
|
|
309
|
+
const lastMatch = await scanLogFileForId(filePath, id);
|
|
303
310
|
if (lastMatch !== null) {
|
|
304
311
|
addToCache(lastMatch);
|
|
305
312
|
observeSessionLog(lastMatch);
|
|
@@ -312,6 +319,37 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
|
|
|
312
319
|
return null;
|
|
313
320
|
}
|
|
314
321
|
|
|
322
|
+
async function scanLogFileForId(filePath: string, id: number): Promise<CapturedLog | null> {
|
|
323
|
+
let lastMatch: CapturedLog | null = null;
|
|
324
|
+
const fileStream = createInterface({
|
|
325
|
+
input: createReadStream(filePath),
|
|
326
|
+
crlfDelay: Infinity,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
for await (const line of fileStream) {
|
|
330
|
+
if (line.trim() === "") continue;
|
|
331
|
+
try {
|
|
332
|
+
const parsed: unknown = JSON.parse(line);
|
|
333
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
334
|
+
const desc = Object.getOwnPropertyDescriptor(parsed, "id");
|
|
335
|
+
if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
|
|
336
|
+
const result = CapturedLogSchema.safeParse(parsed);
|
|
337
|
+
if (result.success) {
|
|
338
|
+
lastMatch = normalizeLogSession(result.data);
|
|
339
|
+
if (result.data.responseStatus !== null) {
|
|
340
|
+
return lastMatch;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
// Skip malformed lines
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return lastMatch;
|
|
351
|
+
}
|
|
352
|
+
|
|
315
353
|
export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog[] {
|
|
316
354
|
// Cache maintains insertion order (sorted by ID since logs are added in ID order)
|
|
317
355
|
// Use spread instead of Array.from for slightly better performance
|
|
@@ -326,43 +364,26 @@ function matchesLogFilters(log: CapturedLog, sessionId?: string, model?: string)
|
|
|
326
364
|
return true;
|
|
327
365
|
}
|
|
328
366
|
|
|
329
|
-
|
|
330
|
-
filteredMemoryCount: number,
|
|
331
|
-
offset: number,
|
|
332
|
-
limit: number,
|
|
333
|
-
): boolean {
|
|
334
|
-
return offset + limit <= filteredMemoryCount;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function paginateLogs(logs: CapturedLog[], offset: number, limit: number): CapturedLog[] {
|
|
338
|
-
return logs.slice(offset, offset + limit);
|
|
339
|
-
}
|
|
367
|
+
type PersistedLogVisitor = (log: CapturedLog) => void;
|
|
340
368
|
|
|
341
|
-
async function
|
|
342
|
-
const byId = new Map<number, CapturedLog>();
|
|
369
|
+
async function visitPersistedLogs(visitor: PersistedLogVisitor): Promise<void> {
|
|
343
370
|
const logDir = resolveLogDir();
|
|
344
|
-
if (!existsSync(logDir)) return
|
|
371
|
+
if (!existsSync(logDir)) return;
|
|
345
372
|
|
|
346
373
|
try {
|
|
347
374
|
const entries = await readdirCallback(logDir);
|
|
348
375
|
const files = entries.filter((file) => file.endsWith(".jsonl")).sort();
|
|
349
376
|
for (const file of files) {
|
|
350
|
-
await
|
|
377
|
+
await visitPersistedLogFile(join(logDir, file), visitor);
|
|
351
378
|
}
|
|
352
379
|
} catch (err) {
|
|
353
380
|
logger.error("[store] Failed to list persisted logs:", String(err));
|
|
354
381
|
}
|
|
355
|
-
|
|
356
|
-
for (const log of memoryCache.values()) {
|
|
357
|
-
byId.set(log.id, log);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return byId;
|
|
361
382
|
}
|
|
362
383
|
|
|
363
|
-
async function
|
|
384
|
+
async function visitPersistedLogFile(
|
|
364
385
|
filePath: string,
|
|
365
|
-
|
|
386
|
+
visitor: PersistedLogVisitor,
|
|
366
387
|
): Promise<void> {
|
|
367
388
|
if (!existsSync(filePath)) return;
|
|
368
389
|
|
|
@@ -379,8 +400,7 @@ async function readPersistedLogFile(
|
|
|
379
400
|
const result = CapturedLogSchema.safeParse(parsed);
|
|
380
401
|
if (result.success) {
|
|
381
402
|
const log = normalizeLogSession(result.data);
|
|
382
|
-
|
|
383
|
-
observeSessionLog(log);
|
|
403
|
+
visitor(log);
|
|
384
404
|
}
|
|
385
405
|
} catch {
|
|
386
406
|
// Skip malformed lines.
|
|
@@ -391,25 +411,96 @@ async function readPersistedLogFile(
|
|
|
391
411
|
}
|
|
392
412
|
}
|
|
393
413
|
|
|
414
|
+
function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
|
|
415
|
+
return options.sessionId === undefined && options.model === undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
|
|
419
|
+
return entry.byteOffset >= 0 && entry.byteLength > 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function listLogsPageFromIndex(
|
|
423
|
+
options: ListLogsPageOptions,
|
|
424
|
+
allowRebuild: boolean,
|
|
425
|
+
): Promise<ListLogsPageResult | null> {
|
|
426
|
+
if (!canUseIndexedListPage(options)) return null;
|
|
427
|
+
|
|
428
|
+
let entries = await listIndexEntries();
|
|
429
|
+
if (entries.length === 0 && allowRebuild) {
|
|
430
|
+
await rebuildIndex();
|
|
431
|
+
entries = await listIndexEntries();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const pageStart = options.offset;
|
|
435
|
+
const pageEnd = options.offset + options.limit;
|
|
436
|
+
const pageEntries = entries.slice(pageStart, pageEnd);
|
|
437
|
+
const hasIncompleteOffset = pageEntries.some((entry) => !hasUsableIndexOffset(entry));
|
|
438
|
+
|
|
439
|
+
if (hasIncompleteOffset) {
|
|
440
|
+
if (!allowRebuild) return null;
|
|
441
|
+
await rebuildIndex();
|
|
442
|
+
return await listLogsPageFromIndex(options, false);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const logs: CapturedLog[] = [];
|
|
446
|
+
const includeBodies = options.includeBodies !== false;
|
|
447
|
+
for (const entry of pageEntries) {
|
|
448
|
+
const log = await getLogById(entry.id);
|
|
449
|
+
if (log === null) return null;
|
|
450
|
+
logs.push(prepareLogForList(log, includeBodies));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
logs,
|
|
455
|
+
total: entries.length,
|
|
456
|
+
offset: options.offset,
|
|
457
|
+
limit: options.limit,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
394
461
|
export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLogsPageResult> {
|
|
395
|
-
const
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
462
|
+
const indexedPage = await listLogsPageFromIndex(options, true);
|
|
463
|
+
if (indexedPage !== null) return indexedPage;
|
|
464
|
+
|
|
465
|
+
const seenIds = new Set<number>();
|
|
466
|
+
const pageById = new Map<number, CapturedLog>();
|
|
467
|
+
const pageStart = options.offset;
|
|
468
|
+
const pageEnd = options.offset + options.limit;
|
|
469
|
+
const includeBodies = options.includeBodies !== false;
|
|
470
|
+
let total = 0;
|
|
471
|
+
|
|
472
|
+
const visitLog = (log: CapturedLog): void => {
|
|
473
|
+
if (!matchesLogFilters(log, options.sessionId, options.model)) return;
|
|
474
|
+
|
|
475
|
+
if (!seenIds.has(log.id)) {
|
|
476
|
+
seenIds.add(log.id);
|
|
477
|
+
const position = total;
|
|
478
|
+
total += 1;
|
|
479
|
+
if (position >= pageStart && position < pageEnd) {
|
|
480
|
+
pageById.set(log.id, prepareLogForList(log, includeBodies));
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (pageById.has(log.id)) {
|
|
486
|
+
pageById.set(log.id, prepareLogForList(log, includeBodies));
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
await visitPersistedLogs((log) => {
|
|
491
|
+
observeSessionLog(log);
|
|
492
|
+
visitLog(log);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
for (const log of memoryCache.values()) {
|
|
496
|
+
visitLog(log);
|
|
403
497
|
}
|
|
404
498
|
|
|
405
|
-
const
|
|
406
|
-
const persistedLogs = [...persistedById.values()]
|
|
407
|
-
.filter((log) => matchesLogFilters(log, options.sessionId, options.model))
|
|
408
|
-
.sort((left, right) => left.id - right.id);
|
|
499
|
+
const logs = [...pageById.values()].sort((left, right) => left.id - right.id);
|
|
409
500
|
|
|
410
501
|
return {
|
|
411
|
-
logs
|
|
412
|
-
total
|
|
502
|
+
logs,
|
|
503
|
+
total,
|
|
413
504
|
offset: options.offset,
|
|
414
505
|
limit: options.limit,
|
|
415
506
|
};
|
|
@@ -492,65 +583,41 @@ export async function loadLogsIntoMemory(): Promise<void> {
|
|
|
492
583
|
const logDir = resolveLogDir();
|
|
493
584
|
if (!existsSync(logDir)) return;
|
|
494
585
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const entries = await readdirCallback(logDir);
|
|
500
|
-
const filesWithExt = entries.filter((f: string) => f.endsWith(".jsonl")).sort();
|
|
501
|
-
for (const file of filesWithExt) {
|
|
502
|
-
const filePath = join(logDir, file);
|
|
503
|
-
await loadLogFile(filePath, lastCompletedById);
|
|
504
|
-
}
|
|
505
|
-
} catch (err) {
|
|
506
|
-
logger.error("[store] Failed to read log directory:", String(err));
|
|
507
|
-
}
|
|
586
|
+
const recentCompletedById = new Map<number, CapturedLog>();
|
|
587
|
+
await visitPersistedLogs((log) => {
|
|
588
|
+
rememberRecentCompletedLog(log, recentCompletedById);
|
|
589
|
+
});
|
|
508
590
|
|
|
509
591
|
// Sort by id descending, keep only MAX_MEMORY_CACHE newest completed entries
|
|
510
|
-
const sorted = [...
|
|
511
|
-
for (
|
|
512
|
-
|
|
513
|
-
if (entry !== undefined) addToCache(entry);
|
|
592
|
+
const sorted = [...recentCompletedById.values()].sort((a, b) => b.id - a.id);
|
|
593
|
+
for (const entry of sorted) {
|
|
594
|
+
addToCache(entry);
|
|
514
595
|
}
|
|
515
596
|
|
|
516
597
|
rebuildSessionRegistry(memoryCache.values());
|
|
517
598
|
}
|
|
518
599
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
):
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
600
|
+
function rememberRecentCompletedLog(
|
|
601
|
+
log: CapturedLog,
|
|
602
|
+
recentCompletedById: Map<number, CapturedLog>,
|
|
603
|
+
): void {
|
|
604
|
+
if (log.responseStatus === null) return;
|
|
605
|
+
recentCompletedById.set(log.id, log);
|
|
606
|
+
while (recentCompletedById.size > MAX_MEMORY_CACHE) {
|
|
607
|
+
const oldestId = findSmallestLogId(recentCompletedById);
|
|
608
|
+
if (oldestId === null) return;
|
|
609
|
+
recentCompletedById.delete(oldestId);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
530
612
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const desc = Object.getOwnPropertyDescriptor(parsed, "id");
|
|
537
|
-
if (desc !== undefined && typeof desc.value === "number") {
|
|
538
|
-
const statusDesc = Object.getOwnPropertyDescriptor(parsed, "responseStatus");
|
|
539
|
-
if (statusDesc !== undefined && statusDesc.value !== null) {
|
|
540
|
-
const result = CapturedLogSchema.safeParse(parsed);
|
|
541
|
-
if (result.success) {
|
|
542
|
-
lastCompletedById.set(desc.value, result.data);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
} catch {
|
|
548
|
-
// Skip malformed lines
|
|
549
|
-
}
|
|
613
|
+
function findSmallestLogId(logsById: ReadonlyMap<number, CapturedLog>): number | null {
|
|
614
|
+
let smallestId: number | null = null;
|
|
615
|
+
for (const id of logsById.keys()) {
|
|
616
|
+
if (smallestId === null || id < smallestId) {
|
|
617
|
+
smallestId = id;
|
|
550
618
|
}
|
|
551
|
-
} catch (err) {
|
|
552
|
-
logger.error("[store] Failed to load logs into memory:", String(err));
|
|
553
619
|
}
|
|
620
|
+
return smallestId;
|
|
554
621
|
}
|
|
555
622
|
|
|
556
623
|
export function clearAllLogs(): { cleared: number } {
|
|
@@ -632,40 +699,113 @@ type RewriteLogFileResult = {
|
|
|
632
699
|
removedIds: Set<number>;
|
|
633
700
|
};
|
|
634
701
|
|
|
702
|
+
function tempRewritePath(filePath: string): string {
|
|
703
|
+
return `${filePath}.rewrite-${String(process.pid)}-${String(Date.now())}.tmp`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function removeTempFile(filePath: string): Promise<void> {
|
|
707
|
+
try {
|
|
708
|
+
await unlink(filePath);
|
|
709
|
+
} catch {
|
|
710
|
+
// Best-effort cleanup for temp rewrite files.
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function waitForWriterDrain(writer: WriteStream): Promise<boolean> {
|
|
715
|
+
return new Promise((resolve) => {
|
|
716
|
+
const cleanup = (): void => {
|
|
717
|
+
writer.off("drain", onDrain);
|
|
718
|
+
writer.off("error", onError);
|
|
719
|
+
};
|
|
720
|
+
const onDrain = (): void => {
|
|
721
|
+
cleanup();
|
|
722
|
+
resolve(true);
|
|
723
|
+
};
|
|
724
|
+
const onError = (): void => {
|
|
725
|
+
cleanup();
|
|
726
|
+
resolve(false);
|
|
727
|
+
};
|
|
728
|
+
writer.once("drain", onDrain);
|
|
729
|
+
writer.once("error", onError);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function writeKeptLogLine(writer: WriteStream, line: string): Promise<boolean> {
|
|
734
|
+
if (writer.write(`${line}\n`, "utf-8")) return true;
|
|
735
|
+
return await waitForWriterDrain(writer);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function finishWriter(writer: WriteStream): Promise<boolean> {
|
|
739
|
+
return new Promise((resolve) => {
|
|
740
|
+
const cleanup = (): void => {
|
|
741
|
+
writer.off("finish", onFinish);
|
|
742
|
+
writer.off("error", onError);
|
|
743
|
+
};
|
|
744
|
+
const onFinish = (): void => {
|
|
745
|
+
cleanup();
|
|
746
|
+
resolve(true);
|
|
747
|
+
};
|
|
748
|
+
const onError = (): void => {
|
|
749
|
+
cleanup();
|
|
750
|
+
resolve(false);
|
|
751
|
+
};
|
|
752
|
+
writer.once("finish", onFinish);
|
|
753
|
+
writer.once("error", onError);
|
|
754
|
+
writer.end();
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
635
758
|
async function rewriteLogFileWithoutIds(
|
|
636
759
|
filePath: string,
|
|
637
760
|
ids: ReadonlySet<number>,
|
|
638
761
|
): Promise<RewriteLogFileResult> {
|
|
639
|
-
|
|
762
|
+
const tempPath = tempRewritePath(filePath);
|
|
763
|
+
const writer = createWriteStream(tempPath, { encoding: "utf-8" });
|
|
764
|
+
const removedIds = new Set<number>();
|
|
765
|
+
let keptLineCount = 0;
|
|
766
|
+
let writeOk = true;
|
|
767
|
+
|
|
640
768
|
try {
|
|
641
|
-
|
|
769
|
+
const fileStream = createInterface({
|
|
770
|
+
input: createReadStream(filePath),
|
|
771
|
+
crlfDelay: Infinity,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
for await (const line of fileStream) {
|
|
775
|
+
if (line === "") continue;
|
|
776
|
+
const id = readLogIdFromLine(line);
|
|
777
|
+
if (id !== null && ids.has(id)) {
|
|
778
|
+
removedIds.add(id);
|
|
779
|
+
} else {
|
|
780
|
+
keptLineCount += 1;
|
|
781
|
+
writeOk = await writeKeptLogLine(writer, line);
|
|
782
|
+
if (!writeOk) break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
642
785
|
} catch (err) {
|
|
643
|
-
logger.warn("[store] Failed to
|
|
644
|
-
|
|
786
|
+
logger.warn("[store] Failed to stream log file for deletion:", filePath, String(err));
|
|
787
|
+
writeOk = false;
|
|
645
788
|
}
|
|
646
789
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for (const line of lines) {
|
|
651
|
-
if (line === "") continue;
|
|
652
|
-
const id = readLogIdFromLine(line);
|
|
653
|
-
if (id !== null && ids.has(id)) {
|
|
654
|
-
removedIds.add(id);
|
|
655
|
-
} else {
|
|
656
|
-
keptLines.push(line);
|
|
657
|
-
}
|
|
790
|
+
if (!(await finishWriter(writer)) || !writeOk) {
|
|
791
|
+
await removeTempFile(tempPath);
|
|
792
|
+
return { rewritten: false, removedIds: new Set() };
|
|
658
793
|
}
|
|
659
794
|
|
|
660
|
-
if (removedIds.size === 0)
|
|
795
|
+
if (removedIds.size === 0) {
|
|
796
|
+
await removeTempFile(tempPath);
|
|
797
|
+
return { rewritten: false, removedIds };
|
|
798
|
+
}
|
|
661
799
|
|
|
662
800
|
try {
|
|
663
|
-
if (
|
|
801
|
+
if (keptLineCount === 0) {
|
|
802
|
+
await removeTempFile(tempPath);
|
|
664
803
|
await unlink(filePath);
|
|
665
804
|
} else {
|
|
666
|
-
await
|
|
805
|
+
await rename(tempPath, filePath);
|
|
667
806
|
}
|
|
668
807
|
} catch (err) {
|
|
808
|
+
await removeTempFile(tempPath);
|
|
669
809
|
logger.warn("[store] Failed to rewrite log file after deletion:", filePath, String(err));
|
|
670
810
|
return { rewritten: false, removedIds: new Set() };
|
|
671
811
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import { onLogUpdate, getLogSessionId, listLogsPage } from "../../proxy/store";
|
|
2
|
+
import { compactLogForList, onLogUpdate, getLogSessionId, listLogsPage } from "../../proxy/store";
|
|
3
3
|
import { type CapturedLog } from "../../proxy/schemas";
|
|
4
4
|
|
|
5
|
-
const INITIAL_STREAM_LOG_LIMIT =
|
|
5
|
+
const INITIAL_STREAM_LOG_LIMIT = 100;
|
|
6
6
|
|
|
7
7
|
export const Route = createFileRoute("/api/logs/stream")({
|
|
8
8
|
server: {
|
|
@@ -11,6 +11,7 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
11
11
|
const url = new URL(request.url);
|
|
12
12
|
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
13
13
|
const model = url.searchParams.get("model") ?? undefined;
|
|
14
|
+
const includeBodies = url.searchParams.get("compact") !== "1";
|
|
14
15
|
|
|
15
16
|
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
16
17
|
let cleanedUp = false;
|
|
@@ -21,7 +22,8 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
21
22
|
if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return;
|
|
22
23
|
if (model !== undefined && log.model !== model) return;
|
|
23
24
|
try {
|
|
24
|
-
const
|
|
25
|
+
const payloadLog = includeBodies ? log : compactLogForList(log);
|
|
26
|
+
const data = `data: ${JSON.stringify({ type: "update", log: payloadLog })}\n\n`;
|
|
25
27
|
controllerRef.enqueue(new TextEncoder().encode(data));
|
|
26
28
|
} catch {
|
|
27
29
|
cleanup();
|
|
@@ -59,6 +61,7 @@ export const Route = createFileRoute("/api/logs/stream")({
|
|
|
59
61
|
model,
|
|
60
62
|
offset: 0,
|
|
61
63
|
limit: INITIAL_STREAM_LOG_LIMIT,
|
|
64
|
+
includeBodies,
|
|
62
65
|
});
|
|
63
66
|
const logs = result.logs;
|
|
64
67
|
const initData = `data: ${JSON.stringify({ type: "init", logs })}\n\n`;
|
package/src/routes/api/logs.ts
CHANGED
|
@@ -49,8 +49,11 @@ export const Route = createFileRoute("/api/logs")({
|
|
|
49
49
|
const model = url.searchParams.get("model") ?? undefined;
|
|
50
50
|
const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);
|
|
51
51
|
const limit = parsePositiveInt(url.searchParams.get("limit"), 50);
|
|
52
|
+
const includeBodies = url.searchParams.get("compact") !== "1";
|
|
52
53
|
|
|
53
|
-
return Response.json(
|
|
54
|
+
return Response.json(
|
|
55
|
+
await listLogsPage({ sessionId, model, offset, limit, includeBodies }),
|
|
56
|
+
);
|
|
54
57
|
},
|
|
55
58
|
DELETE: async ({ request }: { request: Request }) => {
|
|
56
59
|
let body: z.infer<typeof DeleteBodySchema> = undefined;
|