@tonyclaw/agent-inspector 2.0.18 → 2.0.19

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 (72) 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-D-90PIPw.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CxRDWacg.js +114 -0
  5. package/.output/public/assets/ReplayDialog-DzlLYoTO.js +1 -0
  6. package/.output/public/assets/{RequestAnatomy-C_C6ZU3T.js → RequestAnatomy-0hbyf6oa.js} +1 -1
  7. package/.output/public/assets/ResponseView-CKe0tNqr.js +1 -0
  8. package/.output/public/assets/StreamingChunkSequence-enhfdeNh.js +1 -0
  9. package/.output/public/assets/_sessionId-CJ-SusES.js +1 -0
  10. package/.output/public/assets/index-BbFsmQRC.js +1 -0
  11. package/.output/public/assets/index-BfBgrkgU.css +1 -0
  12. package/.output/public/assets/{main-BRXsrxtv.js → main-o9-hg8g1.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-F8lm6PIb.mjs} +3 -2
  17. package/.output/server/_ssr/{CompareDrawer-TgGZLf1J.mjs → CompareDrawer-Cf1yfwfd.mjs} +4 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-B_ppaNmQ.mjs → ProxyViewerContainer-Bmu9867L.mjs} +349 -74
  19. package/.output/server/_ssr/{ReplayDialog-BXlajnMC.mjs → ReplayDialog-DxZfLBOe.mjs} +199 -17
  20. package/.output/server/_ssr/{RequestAnatomy-CCrx09-i.mjs → RequestAnatomy-CbdERGZd.mjs} +4 -3
  21. package/.output/server/_ssr/{ResponseView-BxtMJrov.mjs → ResponseView-CvLd6qxI.mjs} +4 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BlpdSPb8.mjs → StreamingChunkSequence-B_lkRB6o.mjs} +4 -3
  23. package/.output/server/_ssr/{index-BeTMNSJy.mjs → index-ejrSItfz.mjs} +3 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{router-Ddl1tuU7.mjs → router-SbCnbDc2.mjs} +872 -232
  26. package/.output/server/{_tanstack-start-manifest_v-Bvdz_U0f.mjs → _tanstack-start-manifest_v-yx8ftqWz.mjs} +1 -1
  27. package/.output/server/index.mjs +54 -54
  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/knowledge/openclawClient.ts +0 -8
  41. package/src/knowledge/types.ts +0 -2
  42. package/src/lib/export-logs.ts +72 -5
  43. package/src/lib/providerTestContract.ts +0 -1
  44. package/src/lib/useProviders.ts +1 -1
  45. package/src/mcp/server.ts +14 -6
  46. package/src/mcp/toolHandlers.ts +21 -4
  47. package/src/proxy/chunkStorage.ts +2 -4
  48. package/src/proxy/dataDir.ts +3 -3
  49. package/src/proxy/formats/anthropic/index.ts +0 -1
  50. package/src/proxy/formats/anthropic/schemas.ts +0 -2
  51. package/src/proxy/handler.ts +41 -29
  52. package/src/proxy/logFinalizer.ts +93 -11
  53. package/src/proxy/logger.ts +35 -10
  54. package/src/proxy/providers.ts +12 -5
  55. package/src/proxy/rawStreamCapture.ts +85 -0
  56. package/src/proxy/schemas.ts +1 -51
  57. package/src/proxy/sessionRuntime.ts +0 -9
  58. package/src/proxy/socketTracker.ts +15 -8
  59. package/src/proxy/store.ts +414 -13
  60. package/src/routes/api/logs.$id.replay.ts +66 -1
  61. package/src/routes/api/logs.stream.ts +11 -3
  62. package/src/routes/api/logs.ts +51 -17
  63. package/src/routes/api/providers.$providerId.ts +10 -3
  64. package/src/routes/api/providers.ts +6 -3
  65. package/.output/public/assets/ProxyViewerContainer-BG7rc33d.js +0 -114
  66. package/.output/public/assets/ReplayDialog-DK6SPv8X.js +0 -1
  67. package/.output/public/assets/ResponseView-Bqpra4Zw.js +0 -1
  68. package/.output/public/assets/StreamingChunkSequence-CdPbpprq.js +0 -1
  69. package/.output/public/assets/_sessionId-CsNARHj5.js +0 -1
  70. package/.output/public/assets/index-37NfO0zx.js +0 -1
  71. package/.output/public/assets/index-D_nZj9Vt.css +0 -1
  72. package/src/components/ui/json-expansion-button.tsx +0 -56
@@ -1,11 +1,25 @@
1
- import { readFileSync, existsSync, createReadStream } from "node:fs";
2
- import { open } from "node:fs/promises";
1
+ import { readFileSync, existsSync, createReadStream, readdirSync, statSync } from "node:fs";
2
+ import { open, readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { readdir as readdirCallback } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { join } from "node:path";
7
- import { appendLogEntry, resolveLogDir, logger } from "./logger";
8
- import { addToIndex, findInIndex, getNextLogId, getCurrentLogFile } from "./logIndex";
7
+ import {
8
+ appendLogEntry,
9
+ resolveLogDir,
10
+ logger,
11
+ runWithFlushedLogWriteLock,
12
+ runWithLogWriteLock,
13
+ } from "./logger";
14
+ import { getChunksDir } from "./chunkStorage";
15
+ import {
16
+ addToIndex,
17
+ findInIndex,
18
+ flushIndex,
19
+ getNextLogId,
20
+ getCurrentLogFile,
21
+ rebuildIndex,
22
+ } from "./logIndex";
9
23
  import { writeChunks } from "./chunkStorage";
10
24
  import type { CapturedLog } from "./schemas";
11
25
  import { CapturedLogSchema } from "./schemas";
@@ -27,6 +41,42 @@ export { getLogSessionId, getSessionSnapshots } from "./sessionSupervisor";
27
41
 
28
42
  const MAX_MEMORY_CACHE = 100;
29
43
 
44
+ export type LogStorageStats = {
45
+ memoryCount: number;
46
+ logDir: string;
47
+ logFileCount: number;
48
+ logBytes: number;
49
+ chunkDir: string;
50
+ chunkFileCount: number;
51
+ chunkBytes: number;
52
+ };
53
+
54
+ export type ClearPersistedLogStorageResult = {
55
+ cleared: number;
56
+ logFilesDeleted: number;
57
+ chunkFilesDeleted: number;
58
+ };
59
+
60
+ export type ClearPersistedLogsByIdsResult = {
61
+ cleared: number;
62
+ logFilesRewritten: number;
63
+ chunkFilesDeleted: number;
64
+ };
65
+
66
+ export type ListLogsPageOptions = {
67
+ sessionId?: string;
68
+ model?: string;
69
+ offset: number;
70
+ limit: number;
71
+ };
72
+
73
+ export type ListLogsPageResult = {
74
+ logs: CapturedLog[];
75
+ total: number;
76
+ offset: number;
77
+ limit: number;
78
+ };
79
+
30
80
  // Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
31
81
  const memoryCache: Map<number, CapturedLog> = new Map();
32
82
 
@@ -88,9 +138,11 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
88
138
  streamingChunksPath,
89
139
  };
90
140
 
91
- const logFile = getCurrentLogFile();
92
- appendLogEntry(log);
93
- await addToIndex(id, logFile, -1, -1);
141
+ await runWithLogWriteLock(async () => {
142
+ const logFile = getCurrentLogFile();
143
+ appendLogEntry(log);
144
+ await addToIndex(id, logFile, -1, -1);
145
+ });
94
146
 
95
147
  addToCache(log);
96
148
  observeSessionLog(log, session.source);
@@ -154,9 +206,11 @@ export async function createLog(
154
206
  };
155
207
 
156
208
  // Write to disk and update index
157
- const logFile = getCurrentLogFile();
158
- appendLogEntry(log);
159
- await addToIndex(id, logFile, -1, -1); // line numbers not tracked precisely
209
+ await runWithLogWriteLock(async () => {
210
+ const logFile = getCurrentLogFile();
211
+ appendLogEntry(log);
212
+ await addToIndex(id, logFile, -1, -1); // line numbers not tracked precisely
213
+ });
160
214
 
161
215
  // Add to memory cache
162
216
  addToCache(log);
@@ -262,12 +316,105 @@ export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog
262
316
  // Cache maintains insertion order (sorted by ID since logs are added in ID order)
263
317
  // Use spread instead of Array.from for slightly better performance
264
318
  return [...memoryCache.values()].filter((l) => {
265
- if (sessionId !== undefined && getLogSessionId(l) !== sessionId) return false;
266
- if (model !== undefined && l.model !== model) return false;
267
- return true;
319
+ return matchesLogFilters(l, sessionId, model);
268
320
  });
269
321
  }
270
322
 
323
+ function matchesLogFilters(log: CapturedLog, sessionId?: string, model?: string): boolean {
324
+ if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return false;
325
+ if (model !== undefined && log.model !== model) return false;
326
+ return true;
327
+ }
328
+
329
+ function requestedWindowFitsMemory(
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
+ }
340
+
341
+ async function readPersistedLogsById(): Promise<Map<number, CapturedLog>> {
342
+ const byId = new Map<number, CapturedLog>();
343
+ const logDir = resolveLogDir();
344
+ if (!existsSync(logDir)) return byId;
345
+
346
+ try {
347
+ const entries = await readdirCallback(logDir);
348
+ const files = entries.filter((file) => file.endsWith(".jsonl")).sort();
349
+ for (const file of files) {
350
+ await readPersistedLogFile(join(logDir, file), byId);
351
+ }
352
+ } catch (err) {
353
+ logger.error("[store] Failed to list persisted logs:", String(err));
354
+ }
355
+
356
+ for (const log of memoryCache.values()) {
357
+ byId.set(log.id, log);
358
+ }
359
+
360
+ return byId;
361
+ }
362
+
363
+ async function readPersistedLogFile(
364
+ filePath: string,
365
+ byId: Map<number, CapturedLog>,
366
+ ): Promise<void> {
367
+ if (!existsSync(filePath)) return;
368
+
369
+ try {
370
+ const fileStream = createInterface({
371
+ input: createReadStream(filePath),
372
+ crlfDelay: Infinity,
373
+ });
374
+
375
+ for await (const line of fileStream) {
376
+ if (line.trim() === "") continue;
377
+ try {
378
+ const parsed: unknown = JSON.parse(line);
379
+ const result = CapturedLogSchema.safeParse(parsed);
380
+ if (result.success) {
381
+ const log = normalizeLogSession(result.data);
382
+ byId.set(log.id, log);
383
+ observeSessionLog(log);
384
+ }
385
+ } catch {
386
+ // Skip malformed lines.
387
+ }
388
+ }
389
+ } catch (err) {
390
+ logger.error("[store] Failed to read persisted log file:", filePath, String(err));
391
+ }
392
+ }
393
+
394
+ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLogsPageResult> {
395
+ const memoryLogs = getFilteredLogs(options.sessionId, options.model);
396
+ if (requestedWindowFitsMemory(memoryLogs.length, options.offset, options.limit)) {
397
+ return {
398
+ logs: paginateLogs(memoryLogs, options.offset, options.limit),
399
+ total: memoryLogs.length,
400
+ offset: options.offset,
401
+ limit: options.limit,
402
+ };
403
+ }
404
+
405
+ const persistedById = await readPersistedLogsById();
406
+ const persistedLogs = [...persistedById.values()]
407
+ .filter((log) => matchesLogFilters(log, options.sessionId, options.model))
408
+ .sort((left, right) => left.id - right.id);
409
+
410
+ return {
411
+ logs: paginateLogs(persistedLogs, options.offset, options.limit),
412
+ total: persistedLogs.length,
413
+ offset: options.offset,
414
+ limit: options.limit,
415
+ };
416
+ }
417
+
271
418
  export function getSessions(): string[] {
272
419
  return getSessionIds();
273
420
  }
@@ -280,6 +427,60 @@ export function getModels(): string[] {
280
427
  return [...set];
281
428
  }
282
429
 
430
+ export function getReplayLogsForSource(sourceLogId: number): CapturedLog[] {
431
+ return [...memoryCache.values()].filter((log) => log.replayOfLogId === sourceLogId);
432
+ }
433
+
434
+ function collectDirectoryStats(
435
+ dir: string,
436
+ shouldCount: (fileName: string) => boolean,
437
+ ): { fileCount: number; bytes: number } {
438
+ if (!existsSync(dir)) return { fileCount: 0, bytes: 0 };
439
+ let fileCount = 0;
440
+ let bytes = 0;
441
+ const visit = (currentDir: string, depth: number): void => {
442
+ if (depth > 8) return;
443
+ let entries: string[];
444
+ try {
445
+ entries = readdirSync(currentDir);
446
+ } catch {
447
+ return;
448
+ }
449
+ for (const entry of entries) {
450
+ const path = join(currentDir, entry);
451
+ try {
452
+ const stats = statSync(path);
453
+ if (stats.isDirectory()) {
454
+ visit(path, depth + 1);
455
+ } else if (shouldCount(entry)) {
456
+ fileCount += 1;
457
+ bytes += stats.size;
458
+ }
459
+ } catch {
460
+ // Ignore files that disappear while stats are being collected.
461
+ }
462
+ }
463
+ };
464
+ visit(dir, 0);
465
+ return { fileCount, bytes };
466
+ }
467
+
468
+ export function getLogStorageStats(): LogStorageStats {
469
+ const logDir = resolveLogDir();
470
+ const chunkDir = getChunksDir();
471
+ const logStats = collectDirectoryStats(logDir, (fileName) => fileName.endsWith(".jsonl"));
472
+ const chunkStats = collectDirectoryStats(chunkDir, (fileName) => fileName.endsWith(".json"));
473
+ return {
474
+ memoryCount: memoryCache.size,
475
+ logDir,
476
+ logFileCount: logStats.fileCount,
477
+ logBytes: logStats.bytes,
478
+ chunkDir,
479
+ chunkFileCount: chunkStats.fileCount,
480
+ chunkBytes: chunkStats.bytes,
481
+ };
482
+ }
483
+
283
484
  /**
284
485
  * Load recent completed log entries from all log files into the memory cache.
285
486
  * Called on server startup so the frontend shows existing logs.
@@ -359,6 +560,206 @@ export function clearAllLogs(): { cleared: number } {
359
560
  return { cleared: count };
360
561
  }
361
562
 
563
+ async function deleteMatchingFiles(
564
+ dir: string,
565
+ shouldDelete: (fileName: string) => boolean,
566
+ ): Promise<number> {
567
+ if (!existsSync(dir)) return 0;
568
+
569
+ let deleted = 0;
570
+ let entries: string[];
571
+ try {
572
+ entries = await readdirCallback(dir);
573
+ } catch (err) {
574
+ logger.warn("[store] Failed to list log storage directory:", String(err));
575
+ return deleted;
576
+ }
577
+
578
+ for (const entry of entries) {
579
+ if (!shouldDelete(entry)) continue;
580
+ try {
581
+ await unlink(join(dir, entry));
582
+ deleted += 1;
583
+ } catch (err) {
584
+ logger.warn("[store] Failed to delete log storage file:", entry, String(err));
585
+ }
586
+ }
587
+ return deleted;
588
+ }
589
+
590
+ export async function clearPersistedLogStorage(): Promise<ClearPersistedLogStorageResult> {
591
+ return await runWithFlushedLogWriteLock(async () => {
592
+ await flushIndex();
593
+
594
+ const result = clearAllLogs();
595
+ const [logFilesDeleted, chunkFilesDeleted] = await Promise.all([
596
+ deleteMatchingFiles(
597
+ resolveLogDir(),
598
+ (fileName) => fileName.endsWith(".jsonl") || fileName === "logs.idx",
599
+ ),
600
+ deleteMatchingFiles(
601
+ getChunksDir(),
602
+ (fileName) =>
603
+ fileName.endsWith(".json") || (fileName.startsWith(".") && fileName.endsWith(".tmp")),
604
+ ),
605
+ ]);
606
+
607
+ await rebuildIndex();
608
+
609
+ return {
610
+ cleared: result.cleared,
611
+ logFilesDeleted,
612
+ chunkFilesDeleted,
613
+ };
614
+ });
615
+ }
616
+
617
+ function readLogIdFromLine(line: string): number | null {
618
+ if (line.trim() === "") return null;
619
+ try {
620
+ const parsed: unknown = JSON.parse(line);
621
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
622
+ const idDesc = Object.getOwnPropertyDescriptor(parsed, "id");
623
+ if (idDesc === undefined || typeof idDesc.value !== "number") return null;
624
+ return idDesc.value;
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
629
+
630
+ type RewriteLogFileResult = {
631
+ rewritten: boolean;
632
+ removedIds: Set<number>;
633
+ };
634
+
635
+ async function rewriteLogFileWithoutIds(
636
+ filePath: string,
637
+ ids: ReadonlySet<number>,
638
+ ): Promise<RewriteLogFileResult> {
639
+ let content: string;
640
+ try {
641
+ content = await readFile(filePath, "utf-8");
642
+ } catch (err) {
643
+ logger.warn("[store] Failed to read log file for deletion:", filePath, String(err));
644
+ return { rewritten: false, removedIds: new Set() };
645
+ }
646
+
647
+ const lines = content.split("\n");
648
+ const keptLines: string[] = [];
649
+ const removedIds = new Set<number>();
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
+ }
658
+ }
659
+
660
+ if (removedIds.size === 0) return { rewritten: false, removedIds };
661
+
662
+ try {
663
+ if (keptLines.length === 0) {
664
+ await unlink(filePath);
665
+ } else {
666
+ await writeFile(filePath, `${keptLines.join("\n")}\n`, "utf-8");
667
+ }
668
+ } catch (err) {
669
+ logger.warn("[store] Failed to rewrite log file after deletion:", filePath, String(err));
670
+ return { rewritten: false, removedIds: new Set() };
671
+ }
672
+
673
+ return { rewritten: true, removedIds };
674
+ }
675
+
676
+ type RewriteLogFilesResult = {
677
+ logFilesRewritten: number;
678
+ removedIds: Set<number>;
679
+ };
680
+
681
+ async function rewriteLogFilesWithoutIds(ids: ReadonlySet<number>): Promise<RewriteLogFilesResult> {
682
+ const logDir = resolveLogDir();
683
+ if (!existsSync(logDir)) return { logFilesRewritten: 0, removedIds: new Set() };
684
+
685
+ let entries: string[];
686
+ try {
687
+ entries = await readdirCallback(logDir);
688
+ } catch (err) {
689
+ logger.warn("[store] Failed to list log directory for deletion:", String(err));
690
+ return { logFilesRewritten: 0, removedIds: new Set() };
691
+ }
692
+
693
+ let logFilesRewritten = 0;
694
+ const removedIds = new Set<number>();
695
+ for (const entry of entries) {
696
+ if (!entry.endsWith(".jsonl")) continue;
697
+ const result = await rewriteLogFileWithoutIds(join(logDir, entry), ids);
698
+ if (result.rewritten) logFilesRewritten += 1;
699
+ for (const removedId of result.removedIds) {
700
+ removedIds.add(removedId);
701
+ }
702
+ }
703
+ return { logFilesRewritten, removedIds };
704
+ }
705
+
706
+ async function deleteChunkFilesByIds(ids: ReadonlySet<number>): Promise<number> {
707
+ const chunkDir = getChunksDir();
708
+ if (!existsSync(chunkDir)) return 0;
709
+
710
+ let deleted = 0;
711
+ for (const id of ids) {
712
+ const candidates = [`${String(id)}.json`, `.${String(id)}.tmp`];
713
+ for (const candidate of candidates) {
714
+ try {
715
+ await unlink(join(chunkDir, candidate));
716
+ deleted += 1;
717
+ } catch {
718
+ // Missing chunk files are expected for non-streaming logs.
719
+ }
720
+ }
721
+ }
722
+ return deleted;
723
+ }
724
+
725
+ export async function clearPersistedLogsByIds(
726
+ ids: readonly number[],
727
+ ): Promise<ClearPersistedLogsByIdsResult> {
728
+ const uniqueIds = new Set(ids);
729
+ if (uniqueIds.size === 0) {
730
+ return { cleared: 0, logFilesRewritten: 0, chunkFilesDeleted: 0 };
731
+ }
732
+
733
+ return await runWithFlushedLogWriteLock(async () => {
734
+ await flushIndex();
735
+
736
+ const memoryRemovedIds = new Set<number>();
737
+ for (const id of uniqueIds) {
738
+ if (memoryCache.has(id)) memoryRemovedIds.add(id);
739
+ }
740
+ const result = clearLogsByIds([...uniqueIds]);
741
+ const [rewriteResult, chunkFilesDeleted] = await Promise.all([
742
+ rewriteLogFilesWithoutIds(uniqueIds),
743
+ deleteChunkFilesByIds(uniqueIds),
744
+ ]);
745
+
746
+ await rebuildIndex();
747
+
748
+ const clearedIds = new Set<number>(rewriteResult.removedIds);
749
+ if (result.cleared > 0) {
750
+ for (const id of memoryRemovedIds) {
751
+ clearedIds.add(id);
752
+ }
753
+ }
754
+
755
+ return {
756
+ cleared: clearedIds.size,
757
+ logFilesRewritten: rewriteResult.logFilesRewritten,
758
+ chunkFilesDeleted,
759
+ };
760
+ });
761
+ }
762
+
362
763
  /**
363
764
  * Remove a specific set of log IDs from the in-memory cache. Returns the
364
765
  * number of logs actually removed. IDs not present in the cache are ignored.
@@ -1,6 +1,6 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { z } from "zod";
3
- import { getLogById } from "../../proxy/store";
3
+ import { addTestLogEntry, getLogById } from "../../proxy/store";
4
4
  import { findProviderByModel } from "../../proxy/providers";
5
5
  import { registry } from "../../proxy/formats";
6
6
  import { formatForPath } from "../../proxy/formats";
@@ -39,6 +39,7 @@ type ReplayResponse = {
39
39
  outputTokens?: number;
40
40
  elapsedMs?: number;
41
41
  streaming?: boolean;
42
+ replayLogId?: number;
42
43
  };
43
44
 
44
45
  export const Route = createFileRoute("/api/logs/$id/replay")({
@@ -161,6 +162,37 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
161
162
  const mockLog: typeof log = { ...log };
162
163
  const responseText = formatHandler.extractStream(fullResponse, mockLog, model, false);
163
164
  const tokens = formatHandler.extractTokens(responseText);
165
+ const savedReplayLog = await addTestLogEntry({
166
+ timestamp: new Date().toISOString(),
167
+ method: log.method,
168
+ path: log.path,
169
+ model,
170
+ sessionId: log.sessionId,
171
+ rawRequestBody: modifiedBody,
172
+ responseStatus: upstreamRes.status,
173
+ responseText,
174
+ inputTokens: tokens.inputTokens ?? null,
175
+ outputTokens: tokens.outputTokens ?? null,
176
+ cacheCreationInputTokens: tokens.cacheCreationInputTokens ?? null,
177
+ cacheReadInputTokens: tokens.cacheReadInputTokens ?? null,
178
+ elapsedMs,
179
+ streaming: true,
180
+ userAgent: log.userAgent,
181
+ origin: log.origin,
182
+ rawHeaders: log.rawHeaders,
183
+ headers: log.headers,
184
+ apiFormat: log.apiFormat,
185
+ isTest: true,
186
+ replayOfLogId: log.id,
187
+ providerName: log.providerName,
188
+ clientPort: log.clientPort,
189
+ clientPid: log.clientPid,
190
+ clientCwd: log.clientCwd,
191
+ clientProjectFolder: log.clientProjectFolder,
192
+ streamingChunks: undefined,
193
+ streamingChunksPath: null,
194
+ error: null,
195
+ });
164
196
 
165
197
  return Response.json({
166
198
  success: true,
@@ -170,10 +202,42 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
170
202
  outputTokens: tokens.outputTokens ?? undefined,
171
203
  elapsedMs,
172
204
  streaming: true,
205
+ replayLogId: savedReplayLog.id,
173
206
  } satisfies ReplayResponse);
174
207
  } else {
175
208
  const responseText = await upstreamRes.text();
176
209
  const tokens = formatHandler.extractTokens(responseText);
210
+ const savedReplayLog = await addTestLogEntry({
211
+ timestamp: new Date().toISOString(),
212
+ method: log.method,
213
+ path: log.path,
214
+ model,
215
+ sessionId: log.sessionId,
216
+ rawRequestBody: modifiedBody,
217
+ responseStatus: upstreamRes.status,
218
+ responseText,
219
+ inputTokens: tokens.inputTokens ?? null,
220
+ outputTokens: tokens.outputTokens ?? null,
221
+ cacheCreationInputTokens: tokens.cacheCreationInputTokens ?? null,
222
+ cacheReadInputTokens: tokens.cacheReadInputTokens ?? null,
223
+ elapsedMs,
224
+ streaming: false,
225
+ userAgent: log.userAgent,
226
+ origin: log.origin,
227
+ rawHeaders: log.rawHeaders,
228
+ headers: log.headers,
229
+ apiFormat: log.apiFormat,
230
+ isTest: true,
231
+ replayOfLogId: log.id,
232
+ providerName: log.providerName,
233
+ clientPort: log.clientPort,
234
+ clientPid: log.clientPid,
235
+ clientCwd: log.clientCwd,
236
+ clientProjectFolder: log.clientProjectFolder,
237
+ streamingChunks: undefined,
238
+ streamingChunksPath: null,
239
+ error: null,
240
+ });
177
241
 
178
242
  return Response.json({
179
243
  success: true,
@@ -183,6 +247,7 @@ export const Route = createFileRoute("/api/logs/$id/replay")({
183
247
  outputTokens: tokens.outputTokens ?? undefined,
184
248
  elapsedMs,
185
249
  streaming: false,
250
+ replayLogId: savedReplayLog.id,
186
251
  } satisfies ReplayResponse);
187
252
  }
188
253
  },
@@ -1,7 +1,9 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { onLogUpdate, getFilteredLogs, getLogSessionId } from "../../proxy/store";
2
+ import { onLogUpdate, getLogSessionId, listLogsPage } from "../../proxy/store";
3
3
  import { type CapturedLog } from "../../proxy/schemas";
4
4
 
5
+ const INITIAL_STREAM_LOG_LIMIT = 500;
6
+
5
7
  export const Route = createFileRoute("/api/logs/stream")({
6
8
  server: {
7
9
  handlers: {
@@ -49,10 +51,16 @@ export const Route = createFileRoute("/api/logs/stream")({
49
51
  request.signal.addEventListener("abort", cleanup, { once: true });
50
52
 
51
53
  const stream = new ReadableStream<Uint8Array>({
52
- start(controller) {
54
+ async start(controller) {
53
55
  controllerRef = controller;
54
56
  // Send initial batch of logs immediately
55
- const logs = getFilteredLogs(sessionId, model);
57
+ const result = await listLogsPage({
58
+ sessionId,
59
+ model,
60
+ offset: 0,
61
+ limit: INITIAL_STREAM_LOG_LIMIT,
62
+ });
63
+ const logs = result.logs;
56
64
  const initData = `data: ${JSON.stringify({ type: "init", logs })}\n\n`;
57
65
  controller.enqueue(new TextEncoder().encode(initData));
58
66
  },
@@ -1,6 +1,12 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { z } from "zod";
3
- import { clearAllLogs, clearLogsByIds, getFilteredLogs } from "../../proxy/store";
3
+ import {
4
+ clearPersistedLogStorage,
5
+ clearPersistedLogsByIds,
6
+ getLogStorageStats,
7
+ getReplayLogsForSource,
8
+ listLogsPage,
9
+ } from "../../proxy/store";
4
10
 
5
11
  const DeleteBodySchema = z
6
12
  .object({
@@ -8,25 +14,43 @@ const DeleteBodySchema = z
8
14
  })
9
15
  .optional();
10
16
 
17
+ function parseNonNegativeInt(value: string | null, fallback: number): number {
18
+ if (value === null) return fallback;
19
+ const parsed = Number(value);
20
+ if (!Number.isInteger(parsed) || parsed < 0) return fallback;
21
+ return parsed;
22
+ }
23
+
24
+ function parsePositiveInt(value: string | null, fallback: number): number {
25
+ if (value === null) return fallback;
26
+ const parsed = Number(value);
27
+ if (!Number.isInteger(parsed) || parsed < 1) return fallback;
28
+ return parsed;
29
+ }
30
+
11
31
  export const Route = createFileRoute("/api/logs")({
12
32
  server: {
13
33
  handlers: {
14
- GET: ({ request }: { request: Request }) => {
34
+ GET: async ({ request }: { request: Request }) => {
15
35
  const url = new URL(request.url);
36
+ if (url.searchParams.get("stats") === "1") {
37
+ return Response.json(getLogStorageStats());
38
+ }
39
+ const replayOf = url.searchParams.get("replayOf");
40
+ if (replayOf !== null) {
41
+ const id = Number(replayOf);
42
+ if (!Number.isInteger(id) || id <= 0) {
43
+ return Response.json({ error: "Invalid replayOf log ID" }, { status: 400 });
44
+ }
45
+ const logs = getReplayLogsForSource(id);
46
+ return Response.json({ logs, total: logs.length, offset: 0, limit: logs.length });
47
+ }
16
48
  const sessionId = url.searchParams.get("sessionId") ?? undefined;
17
49
  const model = url.searchParams.get("model") ?? undefined;
18
- const offset = Number(url.searchParams.get("offset") ?? 0);
19
- const limit = Number(url.searchParams.get("limit") ?? 50);
50
+ const offset = parseNonNegativeInt(url.searchParams.get("offset"), 0);
51
+ const limit = parsePositiveInt(url.searchParams.get("limit"), 50);
20
52
 
21
- const allLogs = getFilteredLogs(sessionId, model);
22
- const paginatedLogs = allLogs.slice(offset, offset + limit);
23
-
24
- return Response.json({
25
- logs: paginatedLogs,
26
- total: allLogs.length,
27
- offset,
28
- limit,
29
- });
53
+ return Response.json(await listLogsPage({ sessionId, model, offset, limit }));
30
54
  },
31
55
  DELETE: async ({ request }: { request: Request }) => {
32
56
  let body: z.infer<typeof DeleteBodySchema> = undefined;
@@ -48,11 +72,21 @@ export const Route = createFileRoute("/api/logs")({
48
72
  }
49
73
 
50
74
  if (body?.ids !== undefined && body.ids.length > 0) {
51
- const result = clearLogsByIds(body.ids);
52
- return Response.json({ success: true, cleared: result.cleared });
75
+ const result = await clearPersistedLogsByIds(body.ids);
76
+ return Response.json({
77
+ success: true,
78
+ cleared: result.cleared,
79
+ logFilesRewritten: result.logFilesRewritten,
80
+ chunkFilesDeleted: result.chunkFilesDeleted,
81
+ });
53
82
  }
54
- const result = clearAllLogs();
55
- return Response.json({ success: true, cleared: result.cleared });
83
+ const result = await clearPersistedLogStorage();
84
+ return Response.json({
85
+ success: true,
86
+ cleared: result.cleared,
87
+ logFilesDeleted: result.logFilesDeleted,
88
+ chunkFilesDeleted: result.chunkFilesDeleted,
89
+ });
56
90
  },
57
91
  },
58
92
  },