@tonyclaw/agent-inspector 2.0.22 → 2.0.23

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 (27) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-NEdpe1wl.js → CompareDrawer-DnYQtd8R.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-Dwhi4Q1c.js → ProxyViewerContainer-BTYGkg36.js} +4 -4
  4. package/.output/public/assets/{ReplayDialog-DijzJwaQ.js → ReplayDialog-B-3V95xT.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CYbosGYy.js → RequestAnatomy-CT8hLGLa.js} +1 -1
  6. package/.output/public/assets/{ResponseView-B3dPM63X.js → ResponseView-DK3CYom0.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-DYDx3xRY.js → StreamingChunkSequence-D3jJutjp.js} +1 -1
  8. package/.output/public/assets/_sessionId-DpdfAzbo.js +1 -0
  9. package/.output/public/assets/index-BpJNbm9G.js +1 -0
  10. package/.output/public/assets/{main-N-sjHJUM.js → main-hF_572U6.js} +2 -2
  11. package/.output/server/{_sessionId-BR46y_bO.mjs → _sessionId-BqhNE2M8.mjs} +2 -2
  12. package/.output/server/_ssr/{CompareDrawer-DDxpmdPs.mjs → CompareDrawer-C4LQW4S1.mjs} +2 -2
  13. package/.output/server/_ssr/{ProxyViewerContainer-B1zAefzz.mjs → ProxyViewerContainer-BKInvawk.mjs} +7 -7
  14. package/.output/server/_ssr/{ReplayDialog-DleMFvMx.mjs → ReplayDialog-C3ZiRihL.mjs} +3 -3
  15. package/.output/server/_ssr/{RequestAnatomy-B-P9tC3D.mjs → RequestAnatomy-DlVAPIQZ.mjs} +2 -2
  16. package/.output/server/_ssr/{ResponseView-DWfGn0i3.mjs → ResponseView-nMS0rkxu.mjs} +2 -2
  17. package/.output/server/_ssr/{StreamingChunkSequence-C8kFF6xJ.mjs → StreamingChunkSequence-PDWqrNiO.mjs} +2 -2
  18. package/.output/server/_ssr/{index-DzENi2RO.mjs → index-Cd1zD-lo.mjs} +2 -2
  19. package/.output/server/_ssr/index.mjs +2 -2
  20. package/.output/server/_ssr/{router-Dy1VEjxy.mjs → router-R9gm7_sh.mjs} +150 -76
  21. package/.output/server/{_tanstack-start-manifest_v-CyzuAD8A.mjs → _tanstack-start-manifest_v-BnLVH_EX.mjs} +1 -1
  22. package/.output/server/index.mjs +57 -57
  23. package/package.json +1 -1
  24. package/src/proxy/store.ts +198 -135
  25. package/src/routes/api/logs.stream.ts +1 -1
  26. package/.output/public/assets/_sessionId-b1KDnByv.js +0 -1
  27. package/.output/public/assets/index-CUyRKgII.js +0 -1
@@ -1,5 +1,12 @@
1
- import { readFileSync, existsSync, createReadStream, readdirSync, statSync } from "node:fs";
2
- import { open, readFile, unlink, writeFile } from "node:fs/promises";
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";
@@ -273,33 +280,7 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
273
280
  return null;
274
281
  }
275
282
 
276
- // Fallback: scan file for matching ID (for entries written before index rebuild)
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
- }
283
+ const lastMatch = await scanLogFileForId(filePath, id);
303
284
  if (lastMatch !== null) {
304
285
  addToCache(lastMatch);
305
286
  observeSessionLog(lastMatch);
@@ -312,6 +293,37 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
312
293
  return null;
313
294
  }
314
295
 
296
+ async function scanLogFileForId(filePath: string, id: number): Promise<CapturedLog | null> {
297
+ let lastMatch: CapturedLog | null = null;
298
+ const fileStream = createInterface({
299
+ input: createReadStream(filePath),
300
+ crlfDelay: Infinity,
301
+ });
302
+
303
+ for await (const line of fileStream) {
304
+ if (line.trim() === "") continue;
305
+ try {
306
+ const parsed: unknown = JSON.parse(line);
307
+ if (typeof parsed === "object" && parsed !== null) {
308
+ const desc = Object.getOwnPropertyDescriptor(parsed, "id");
309
+ if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
310
+ const result = CapturedLogSchema.safeParse(parsed);
311
+ if (result.success) {
312
+ lastMatch = normalizeLogSession(result.data);
313
+ if (result.data.responseStatus !== null) {
314
+ return lastMatch;
315
+ }
316
+ }
317
+ }
318
+ }
319
+ } catch {
320
+ // Skip malformed lines
321
+ }
322
+ }
323
+
324
+ return lastMatch;
325
+ }
326
+
315
327
  export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog[] {
316
328
  // Cache maintains insertion order (sorted by ID since logs are added in ID order)
317
329
  // Use spread instead of Array.from for slightly better performance
@@ -326,43 +338,26 @@ function matchesLogFilters(log: CapturedLog, sessionId?: string, model?: string)
326
338
  return true;
327
339
  }
328
340
 
329
- function requestedWindowFitsMemory(
330
- filteredMemoryCount: number,
331
- offset: number,
332
- limit: number,
333
- ): boolean {
334
- return offset + limit <= filteredMemoryCount;
335
- }
341
+ type PersistedLogVisitor = (log: CapturedLog) => void;
336
342
 
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
+ async function visitPersistedLogs(visitor: PersistedLogVisitor): Promise<void> {
343
344
  const logDir = resolveLogDir();
344
- if (!existsSync(logDir)) return byId;
345
+ if (!existsSync(logDir)) return;
345
346
 
346
347
  try {
347
348
  const entries = await readdirCallback(logDir);
348
349
  const files = entries.filter((file) => file.endsWith(".jsonl")).sort();
349
350
  for (const file of files) {
350
- await readPersistedLogFile(join(logDir, file), byId);
351
+ await visitPersistedLogFile(join(logDir, file), visitor);
351
352
  }
352
353
  } catch (err) {
353
354
  logger.error("[store] Failed to list persisted logs:", String(err));
354
355
  }
355
-
356
- for (const log of memoryCache.values()) {
357
- byId.set(log.id, log);
358
- }
359
-
360
- return byId;
361
356
  }
362
357
 
363
- async function readPersistedLogFile(
358
+ async function visitPersistedLogFile(
364
359
  filePath: string,
365
- byId: Map<number, CapturedLog>,
360
+ visitor: PersistedLogVisitor,
366
361
  ): Promise<void> {
367
362
  if (!existsSync(filePath)) return;
368
363
 
@@ -379,8 +374,7 @@ async function readPersistedLogFile(
379
374
  const result = CapturedLogSchema.safeParse(parsed);
380
375
  if (result.success) {
381
376
  const log = normalizeLogSession(result.data);
382
- byId.set(log.id, log);
383
- observeSessionLog(log);
377
+ visitor(log);
384
378
  }
385
379
  } catch {
386
380
  // Skip malformed lines.
@@ -392,24 +386,44 @@ async function readPersistedLogFile(
392
386
  }
393
387
 
394
388
  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
- };
389
+ const seenIds = new Set<number>();
390
+ const pageById = new Map<number, CapturedLog>();
391
+ const pageStart = options.offset;
392
+ const pageEnd = options.offset + options.limit;
393
+ let total = 0;
394
+
395
+ const visitLog = (log: CapturedLog): void => {
396
+ if (!matchesLogFilters(log, options.sessionId, options.model)) return;
397
+
398
+ if (!seenIds.has(log.id)) {
399
+ seenIds.add(log.id);
400
+ const position = total;
401
+ total += 1;
402
+ if (position >= pageStart && position < pageEnd) {
403
+ pageById.set(log.id, log);
404
+ }
405
+ return;
406
+ }
407
+
408
+ if (pageById.has(log.id)) {
409
+ pageById.set(log.id, log);
410
+ }
411
+ };
412
+
413
+ await visitPersistedLogs((log) => {
414
+ observeSessionLog(log);
415
+ visitLog(log);
416
+ });
417
+
418
+ for (const log of memoryCache.values()) {
419
+ visitLog(log);
403
420
  }
404
421
 
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);
422
+ const logs = [...pageById.values()].sort((left, right) => left.id - right.id);
409
423
 
410
424
  return {
411
- logs: paginateLogs(persistedLogs, options.offset, options.limit),
412
- total: persistedLogs.length,
425
+ logs,
426
+ total,
413
427
  offset: options.offset,
414
428
  limit: options.limit,
415
429
  };
@@ -492,65 +506,41 @@ export async function loadLogsIntoMemory(): Promise<void> {
492
506
  const logDir = resolveLogDir();
493
507
  if (!existsSync(logDir)) return;
494
508
 
495
- // lastCompletedById: id -> CapturedLog (overwrite on each occurrence)
496
- const lastCompletedById = new Map<number, CapturedLog>();
497
-
498
- try {
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
- }
509
+ const recentCompletedById = new Map<number, CapturedLog>();
510
+ await visitPersistedLogs((log) => {
511
+ rememberRecentCompletedLog(log, recentCompletedById);
512
+ });
508
513
 
509
514
  // Sort by id descending, keep only MAX_MEMORY_CACHE newest completed entries
510
- const sorted = [...lastCompletedById.values()].sort((a, b) => b.id - a.id);
511
- for (let i = 0; i < Math.min(sorted.length, MAX_MEMORY_CACHE); i++) {
512
- const entry = sorted[i];
513
- if (entry !== undefined) addToCache(entry);
515
+ const sorted = [...recentCompletedById.values()].sort((a, b) => b.id - a.id);
516
+ for (const entry of sorted) {
517
+ addToCache(entry);
514
518
  }
515
519
 
516
520
  rebuildSessionRegistry(memoryCache.values());
517
521
  }
518
522
 
519
- async function loadLogFile(
520
- filePath: string,
521
- lastCompletedById: Map<number, CapturedLog>,
522
- ): Promise<void> {
523
- if (!existsSync(filePath)) return;
524
-
525
- try {
526
- const fileStream = createInterface({
527
- input: createReadStream(filePath),
528
- crlfDelay: Infinity,
529
- });
523
+ function rememberRecentCompletedLog(
524
+ log: CapturedLog,
525
+ recentCompletedById: Map<number, CapturedLog>,
526
+ ): void {
527
+ if (log.responseStatus === null) return;
528
+ recentCompletedById.set(log.id, log);
529
+ while (recentCompletedById.size > MAX_MEMORY_CACHE) {
530
+ const oldestId = findSmallestLogId(recentCompletedById);
531
+ if (oldestId === null) return;
532
+ recentCompletedById.delete(oldestId);
533
+ }
534
+ }
530
535
 
531
- for await (const line of fileStream) {
532
- if (line.trim() === "") continue;
533
- try {
534
- const parsed: unknown = JSON.parse(line);
535
- if (typeof parsed === "object" && parsed !== null) {
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
- }
536
+ function findSmallestLogId(logsById: ReadonlyMap<number, CapturedLog>): number | null {
537
+ let smallestId: number | null = null;
538
+ for (const id of logsById.keys()) {
539
+ if (smallestId === null || id < smallestId) {
540
+ smallestId = id;
550
541
  }
551
- } catch (err) {
552
- logger.error("[store] Failed to load logs into memory:", String(err));
553
542
  }
543
+ return smallestId;
554
544
  }
555
545
 
556
546
  export function clearAllLogs(): { cleared: number } {
@@ -632,40 +622,113 @@ type RewriteLogFileResult = {
632
622
  removedIds: Set<number>;
633
623
  };
634
624
 
625
+ function tempRewritePath(filePath: string): string {
626
+ return `${filePath}.rewrite-${String(process.pid)}-${String(Date.now())}.tmp`;
627
+ }
628
+
629
+ async function removeTempFile(filePath: string): Promise<void> {
630
+ try {
631
+ await unlink(filePath);
632
+ } catch {
633
+ // Best-effort cleanup for temp rewrite files.
634
+ }
635
+ }
636
+
637
+ function waitForWriterDrain(writer: WriteStream): Promise<boolean> {
638
+ return new Promise((resolve) => {
639
+ const cleanup = (): void => {
640
+ writer.off("drain", onDrain);
641
+ writer.off("error", onError);
642
+ };
643
+ const onDrain = (): void => {
644
+ cleanup();
645
+ resolve(true);
646
+ };
647
+ const onError = (): void => {
648
+ cleanup();
649
+ resolve(false);
650
+ };
651
+ writer.once("drain", onDrain);
652
+ writer.once("error", onError);
653
+ });
654
+ }
655
+
656
+ async function writeKeptLogLine(writer: WriteStream, line: string): Promise<boolean> {
657
+ if (writer.write(`${line}\n`, "utf-8")) return true;
658
+ return await waitForWriterDrain(writer);
659
+ }
660
+
661
+ function finishWriter(writer: WriteStream): Promise<boolean> {
662
+ return new Promise((resolve) => {
663
+ const cleanup = (): void => {
664
+ writer.off("finish", onFinish);
665
+ writer.off("error", onError);
666
+ };
667
+ const onFinish = (): void => {
668
+ cleanup();
669
+ resolve(true);
670
+ };
671
+ const onError = (): void => {
672
+ cleanup();
673
+ resolve(false);
674
+ };
675
+ writer.once("finish", onFinish);
676
+ writer.once("error", onError);
677
+ writer.end();
678
+ });
679
+ }
680
+
635
681
  async function rewriteLogFileWithoutIds(
636
682
  filePath: string,
637
683
  ids: ReadonlySet<number>,
638
684
  ): Promise<RewriteLogFileResult> {
639
- let content: string;
685
+ const tempPath = tempRewritePath(filePath);
686
+ const writer = createWriteStream(tempPath, { encoding: "utf-8" });
687
+ const removedIds = new Set<number>();
688
+ let keptLineCount = 0;
689
+ let writeOk = true;
690
+
640
691
  try {
641
- content = await readFile(filePath, "utf-8");
692
+ const fileStream = createInterface({
693
+ input: createReadStream(filePath),
694
+ crlfDelay: Infinity,
695
+ });
696
+
697
+ for await (const line of fileStream) {
698
+ if (line === "") continue;
699
+ const id = readLogIdFromLine(line);
700
+ if (id !== null && ids.has(id)) {
701
+ removedIds.add(id);
702
+ } else {
703
+ keptLineCount += 1;
704
+ writeOk = await writeKeptLogLine(writer, line);
705
+ if (!writeOk) break;
706
+ }
707
+ }
642
708
  } catch (err) {
643
- logger.warn("[store] Failed to read log file for deletion:", filePath, String(err));
644
- return { rewritten: false, removedIds: new Set() };
709
+ logger.warn("[store] Failed to stream log file for deletion:", filePath, String(err));
710
+ writeOk = false;
645
711
  }
646
712
 
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
- }
713
+ if (!(await finishWriter(writer)) || !writeOk) {
714
+ await removeTempFile(tempPath);
715
+ return { rewritten: false, removedIds: new Set() };
658
716
  }
659
717
 
660
- if (removedIds.size === 0) return { rewritten: false, removedIds };
718
+ if (removedIds.size === 0) {
719
+ await removeTempFile(tempPath);
720
+ return { rewritten: false, removedIds };
721
+ }
661
722
 
662
723
  try {
663
- if (keptLines.length === 0) {
724
+ if (keptLineCount === 0) {
725
+ await removeTempFile(tempPath);
664
726
  await unlink(filePath);
665
727
  } else {
666
- await writeFile(filePath, `${keptLines.join("\n")}\n`, "utf-8");
728
+ await rename(tempPath, filePath);
667
729
  }
668
730
  } catch (err) {
731
+ await removeTempFile(tempPath);
669
732
  logger.warn("[store] Failed to rewrite log file after deletion:", filePath, String(err));
670
733
  return { rewritten: false, removedIds: new Set() };
671
734
  }
@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
2
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;
5
+ const INITIAL_STREAM_LOG_LIMIT = 100;
6
6
 
7
7
  export const Route = createFileRoute("/api/logs/stream")({
8
8
  server: {
@@ -1 +0,0 @@
1
- import{R as s,j as e}from"./main-N-sjHJUM.js";import{P as i}from"./ProxyViewerContainer-Dwhi4Q1c.js";function t(){const{sessionId:o}=s.useParams();return e.jsx(i,{initialSessionId:o},o)}export{t as component};
@@ -1 +0,0 @@
1
- import{P as o}from"./ProxyViewerContainer-Dwhi4Q1c.js";import"./main-N-sjHJUM.js";const r=o;export{r as component};