@roberttlange/agentlens 0.2.3 → 0.3.0

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 (38) hide show
  1. package/dist/browser.js +154 -20
  2. package/dist/browser.js.map +1 -1
  3. package/dist/main.test.js +138 -1
  4. package/dist/main.test.js.map +1 -1
  5. package/node_modules/@agentlens/contracts/dist/index.d.ts +109 -0
  6. package/node_modules/@agentlens/core/dist/__tests__/config.test.js +18 -0
  7. package/node_modules/@agentlens/core/dist/__tests__/config.test.js.map +1 -1
  8. package/node_modules/@agentlens/core/dist/__tests__/index.test.js +370 -2
  9. package/node_modules/@agentlens/core/dist/__tests__/index.test.js.map +1 -1
  10. package/node_modules/@agentlens/core/dist/config.js +33 -0
  11. package/node_modules/@agentlens/core/dist/config.js.map +1 -1
  12. package/node_modules/@agentlens/core/dist/metrics.d.ts +13 -0
  13. package/node_modules/@agentlens/core/dist/metrics.js +98 -2
  14. package/node_modules/@agentlens/core/dist/metrics.js.map +1 -1
  15. package/node_modules/@agentlens/core/dist/sourceProfiles.js +4 -0
  16. package/node_modules/@agentlens/core/dist/sourceProfiles.js.map +1 -1
  17. package/node_modules/@agentlens/core/dist/traceIndex.d.ts +22 -1
  18. package/node_modules/@agentlens/core/dist/traceIndex.js +322 -21
  19. package/node_modules/@agentlens/core/dist/traceIndex.js.map +1 -1
  20. package/node_modules/@agentlens/server/dist/activity-cache.d.ts +6 -3
  21. package/node_modules/@agentlens/server/dist/activity-cache.js +6 -0
  22. package/node_modules/@agentlens/server/dist/activity-cache.js.map +1 -1
  23. package/node_modules/@agentlens/server/dist/activity-cache.test.js +86 -0
  24. package/node_modules/@agentlens/server/dist/activity-cache.test.js.map +1 -1
  25. package/node_modules/@agentlens/server/dist/activity.d.ts +20 -1
  26. package/node_modules/@agentlens/server/dist/activity.js +482 -6
  27. package/node_modules/@agentlens/server/dist/activity.js.map +1 -1
  28. package/node_modules/@agentlens/server/dist/app.d.ts +4 -2
  29. package/node_modules/@agentlens/server/dist/app.js +242 -5
  30. package/node_modules/@agentlens/server/dist/app.js.map +1 -1
  31. package/node_modules/@agentlens/server/dist/app.test.js +669 -8
  32. package/node_modules/@agentlens/server/dist/app.test.js.map +1 -1
  33. package/node_modules/@agentlens/server/dist/web/assets/index-CTFOBaBt.css +1 -0
  34. package/node_modules/@agentlens/server/dist/web/assets/index-CVf00w06.js +52 -0
  35. package/node_modules/@agentlens/server/dist/web/index.html +2 -2
  36. package/package.json +1 -1
  37. package/node_modules/@agentlens/server/dist/web/assets/index-DjwZvHl6.css +0 -1
  38. package/node_modules/@agentlens/server/dist/web/assets/index-Ei_qfgA9.js +0 -52
@@ -25,6 +25,24 @@ const ACTIVITY_BIN_COUNT = 12;
25
25
  const ACTIVE_IDLE_GAP_MS = 20 * 60_000;
26
26
  const MATERIALIZED_TTL_MS = 5 * 60_000;
27
27
  const DIRTY_BATCH_LIMIT = 64;
28
+ const DIRTY_REFRESH_DELAY_MS = 25;
29
+ const WATCH_WRITE_STABILITY_MIN_MS = 35;
30
+ const WATCH_WRITE_STABILITY_MAX_MS = 75;
31
+ const WATCH_WRITE_POLL_INTERVAL_MS = 20;
32
+ const STARTUP_RECENT_TRACE_LIMIT = 120;
33
+ const BACKGROUND_HYDRATE_BATCH_SIZE = 25;
34
+ const BACKGROUND_HYDRATE_DELAY_MS = 25;
35
+ function createInitialStartupStatus() {
36
+ return {
37
+ phase: "cold",
38
+ inspectorReady: false,
39
+ fullReady: false,
40
+ isPartial: false,
41
+ discoveredTraceCount: 0,
42
+ hydratedTraceCount: 0,
43
+ startupError: "",
44
+ };
45
+ }
28
46
  function emptyEventKindCounts() {
29
47
  return {
30
48
  system: 0,
@@ -74,6 +92,21 @@ function buildSessionActivityArtifacts(events) {
74
92
  activeSegments: buildActiveSegmentsFromEventTimestamps(eventTimestamps),
75
93
  };
76
94
  }
95
+ function buildSessionUsageArtifacts(events, agent, config) {
96
+ return {
97
+ eventCount: events.length,
98
+ usagePoints: deriveSessionMetrics(events, agent, config).usagePoints,
99
+ };
100
+ }
101
+ function compactActivityArtifacts(artifacts) {
102
+ if (artifacts.eventTimestamps.length === 0)
103
+ return artifacts;
104
+ return {
105
+ eventCount: artifacts.eventCount,
106
+ eventTimestamps: [],
107
+ activeSegments: artifacts.activeSegments,
108
+ };
109
+ }
77
110
  function normalizeActivityCounts(counts) {
78
111
  let maxCount = 0;
79
112
  for (const count of counts) {
@@ -479,6 +512,7 @@ export class TraceIndex extends EventEmitter {
479
512
  config;
480
513
  entries = new Map();
481
514
  pathToId = new Map();
515
+ discoveredFilesById = new Map();
482
516
  cursorById = new Map();
483
517
  watcher = null;
484
518
  timer = null;
@@ -489,6 +523,11 @@ export class TraceIndex extends EventEmitter {
489
523
  queuedForceFullRefresh = false;
490
524
  dirtyPaths = new Set();
491
525
  forceReparsePaths = new Set();
526
+ pendingHydrationFiles = [];
527
+ pendingHydrationIds = new Set();
528
+ pendingHydrationPaths = new Set();
529
+ pendingHydrationCursor = 0;
530
+ startupStatus = createInitialStartupStatus();
492
531
  adaptiveIntervalMs;
493
532
  perf = {
494
533
  refreshCount: 0,
@@ -532,8 +571,43 @@ export class TraceIndex extends EventEmitter {
532
571
  }
533
572
  async start() {
534
573
  this.started = true;
535
- await this.refresh();
574
+ this.startupStatus = {
575
+ phase: "bootstrapping",
576
+ inspectorReady: false,
577
+ fullReady: false,
578
+ isPartial: false,
579
+ discoveredTraceCount: 0,
580
+ hydratedTraceCount: 0,
581
+ startupError: "",
582
+ };
583
+ this.pendingHydrationFiles = [];
584
+ this.pendingHydrationIds.clear();
585
+ this.pendingHydrationPaths.clear();
586
+ this.pendingHydrationCursor = 0;
536
587
  await this.restartWatcher();
588
+ if (!this.started) {
589
+ if (this.watcher) {
590
+ await this.watcher.close();
591
+ this.watcher = null;
592
+ }
593
+ return;
594
+ }
595
+ try {
596
+ await this.bootstrapRecentTraces();
597
+ }
598
+ catch (error) {
599
+ this.startupStatus = {
600
+ ...this.startupStatus,
601
+ phase: "failed",
602
+ inspectorReady: false,
603
+ fullReady: false,
604
+ isPartial: false,
605
+ startupError: error instanceof Error ? error.message : String(error),
606
+ };
607
+ throw error;
608
+ }
609
+ if (!this.started)
610
+ return;
537
611
  this.scheduleNextRefresh(this.computeBaseIntervalMs());
538
612
  }
539
613
  stop() {
@@ -547,6 +621,11 @@ export class TraceIndex extends EventEmitter {
547
621
  this.watcher = null;
548
622
  }
549
623
  this.forceReparsePaths.clear();
624
+ this.pendingHydrationFiles = [];
625
+ this.pendingHydrationIds.clear();
626
+ this.pendingHydrationPaths.clear();
627
+ this.pendingHydrationCursor = 0;
628
+ this.discoveredFilesById.clear();
550
629
  }
551
630
  async refresh() {
552
631
  this.queuedForceFullRefresh = true;
@@ -564,6 +643,40 @@ export class TraceIndex extends EventEmitter {
564
643
  getStreamVersion() {
565
644
  return this.streamVersion;
566
645
  }
646
+ getStartupStatus() {
647
+ return { ...this.startupStatus };
648
+ }
649
+ getStartupState() {
650
+ return this.getStartupStatus();
651
+ }
652
+ getHydrationProgress(windowStartMs) {
653
+ if (!this.startupStatus.inspectorReady) {
654
+ return {
655
+ ready: false,
656
+ relevantDiscoveredCount: 0,
657
+ relevantHydratedCount: 0,
658
+ percent: 0,
659
+ };
660
+ }
661
+ let relevantDiscoveredCount = 0;
662
+ let relevantHydratedCount = 0;
663
+ for (const file of this.discoveredFilesById.values()) {
664
+ if (file.mtimeMs < windowStartMs)
665
+ continue;
666
+ relevantDiscoveredCount += 1;
667
+ if (!this.pendingHydrationIds.has(file.id)) {
668
+ relevantHydratedCount += 1;
669
+ }
670
+ }
671
+ const ready = relevantHydratedCount >= relevantDiscoveredCount;
672
+ const percent = relevantDiscoveredCount === 0 ? 100 : Math.round((relevantHydratedCount / relevantDiscoveredCount) * 100);
673
+ return {
674
+ ready,
675
+ relevantDiscoveredCount,
676
+ relevantHydratedCount,
677
+ percent,
678
+ };
679
+ }
567
680
  getTopTools(limit = 12) {
568
681
  const counts = new Map();
569
682
  for (const entry of this.entries.values()) {
@@ -576,6 +689,91 @@ export class TraceIndex extends EventEmitter {
576
689
  .slice(0, Math.max(1, limit))
577
690
  .map(([name, count]) => ({ name, count }));
578
691
  }
692
+ async bootstrapRecentTraces() {
693
+ const bootstrapNowMs = nowMs();
694
+ const files = await discoverTraceFiles(this.config);
695
+ this.lastFullRefreshAtMs = bootstrapNowMs;
696
+ this.forceReparsePaths.clear();
697
+ const nextPathToId = new Map();
698
+ const discoveredFilesById = new Map();
699
+ for (const file of files) {
700
+ nextPathToId.set(file.path, file.id);
701
+ discoveredFilesById.set(file.id, file);
702
+ }
703
+ this.pathToId = nextPathToId;
704
+ this.discoveredFilesById = discoveredFilesById;
705
+ const bootstrapFiles = files.slice(0, STARTUP_RECENT_TRACE_LIMIT);
706
+ const pendingFiles = files.slice(bootstrapFiles.length);
707
+ this.pendingHydrationFiles = pendingFiles;
708
+ this.pendingHydrationIds = new Set(pendingFiles.map((file) => file.id));
709
+ this.pendingHydrationPaths = new Set(pendingFiles.map((file) => file.path));
710
+ this.pendingHydrationCursor = 0;
711
+ this.startupStatus = {
712
+ phase: bootstrapFiles.length > 0 ? "bootstrapping" : pendingFiles.length > 0 ? "hydrating" : "ready",
713
+ inspectorReady: false,
714
+ fullReady: pendingFiles.length === 0,
715
+ isPartial: pendingFiles.length > 0,
716
+ discoveredTraceCount: files.length,
717
+ hydratedTraceCount: 0,
718
+ startupError: "",
719
+ };
720
+ const stats = {
721
+ parsedFileCount: 0,
722
+ dirtyPathCount: 0,
723
+ usedFullRefresh: false,
724
+ hadFileMutations: false,
725
+ };
726
+ for (const file of bootstrapFiles) {
727
+ const changed = await this.upsertFile(file, bootstrapNowMs);
728
+ if (changed) {
729
+ stats.parsedFileCount += 1;
730
+ stats.hadFileMutations = true;
731
+ }
732
+ }
733
+ this.startupStatus = {
734
+ ...this.startupStatus,
735
+ phase: pendingFiles.length > 0 ? "hydrating" : "ready",
736
+ inspectorReady: true,
737
+ fullReady: pendingFiles.length === 0,
738
+ isPartial: pendingFiles.length > 0,
739
+ hydratedTraceCount: this.entries.size,
740
+ };
741
+ this.finishMutationBatch(bootstrapNowMs, stats);
742
+ }
743
+ finishMutationBatch(refreshNowMs, stats) {
744
+ this.applyRetention(refreshNowMs);
745
+ this.refreshActivityStatus(refreshNowMs, stats);
746
+ this.perf.trackedFiles = this.entries.size;
747
+ this.perf.dirtyPathQueue = this.dirtyPaths.size;
748
+ const retentionStats = this.buildRetentionStats();
749
+ this.perf.hotTraces = retentionStats.hotTraces;
750
+ this.perf.warmTraces = retentionStats.warmTraces;
751
+ this.perf.coldTraces = retentionStats.coldTraces;
752
+ this.perf.materializedTraces = retentionStats.materializedTraces;
753
+ this.emitOverviewUpdated();
754
+ }
755
+ emitOverviewUpdated() {
756
+ this.emitStream("overview_updated", {
757
+ overview: this.getOverview(),
758
+ startup: this.getStartupStatus(),
759
+ });
760
+ }
761
+ hasPendingHydrationWork() {
762
+ return this.pendingHydrationIds.size > 0;
763
+ }
764
+ updateStartupProgress() {
765
+ const remainingCount = this.pendingHydrationIds.size;
766
+ const discoveredTraceCount = this.startupStatus.discoveredTraceCount;
767
+ this.startupStatus = {
768
+ ...this.startupStatus,
769
+ phase: this.startupStatus.startupError ? "failed" : remainingCount > 0 ? "hydrating" : "ready",
770
+ inspectorReady: true,
771
+ fullReady: remainingCount === 0,
772
+ isPartial: remainingCount > 0,
773
+ discoveredTraceCount,
774
+ hydratedTraceCount: Math.max(0, discoveredTraceCount - remainingCount),
775
+ };
776
+ }
579
777
  buildRetentionStats() {
580
778
  let hotTraces = 0;
581
779
  let warmTraces = 0;
@@ -625,13 +823,14 @@ export class TraceIndex extends EventEmitter {
625
823
  if (roots.length === 0)
626
824
  return;
627
825
  const debounceMs = Math.max(50, this.config.scan.batchDebounceMs);
826
+ const writeStabilityMs = Math.max(WATCH_WRITE_STABILITY_MIN_MS, Math.min(WATCH_WRITE_STABILITY_MAX_MS, debounceMs));
628
827
  this.watcher = chokidar.watch(roots, {
629
828
  ignoreInitial: true,
630
829
  persistent: true,
631
830
  followSymlinks: false,
632
831
  awaitWriteFinish: {
633
- stabilityThreshold: debounceMs,
634
- pollInterval: 40,
832
+ stabilityThreshold: writeStabilityMs,
833
+ pollInterval: WATCH_WRITE_POLL_INTERVAL_MS,
635
834
  },
636
835
  });
637
836
  const onDirty = (rawPath) => {
@@ -651,7 +850,7 @@ export class TraceIndex extends EventEmitter {
651
850
  }
652
851
  }
653
852
  this.perf.dirtyPathQueue = this.dirtyPaths.size;
654
- this.scheduleNextRefresh(debounceMs);
853
+ this.scheduleNextRefresh(DIRTY_REFRESH_DELAY_MS);
655
854
  };
656
855
  this.watcher.on("add", onDirty);
657
856
  this.watcher.on("change", onDirty);
@@ -752,6 +951,8 @@ export class TraceIndex extends EventEmitter {
752
951
  return Array.from(matches.values());
753
952
  }
754
953
  shouldRunFullRefresh(nowMsValue) {
954
+ if (this.hasPendingHydrationWork())
955
+ return false;
755
956
  if (this.lastFullRefreshAtMs <= 0)
756
957
  return true;
757
958
  const fullIntervalMs = Math.max(1_000, this.config.scan.fullRescanIntervalMs);
@@ -781,6 +982,9 @@ export class TraceIndex extends EventEmitter {
781
982
  if (this.refreshPending || this.queuedForceFullRefresh) {
782
983
  this.scheduleNextRefresh(25);
783
984
  }
985
+ else if (this.hasPendingHydrationWork()) {
986
+ this.scheduleNextRefresh(BACKGROUND_HYDRATE_DELAY_MS);
987
+ }
784
988
  else if (this.started) {
785
989
  this.scheduleNextRefresh();
786
990
  }
@@ -822,31 +1026,45 @@ export class TraceIndex extends EventEmitter {
822
1026
  if (useFullRefresh) {
823
1027
  await this.refreshFull(refreshNowMs, stats);
824
1028
  }
825
- else {
1029
+ else if (this.dirtyPaths.size > 0) {
826
1030
  await this.refreshDirty(refreshNowMs, stats);
827
1031
  }
828
- this.applyRetention(refreshNowMs);
829
- this.refreshActivityStatus(refreshNowMs, stats);
830
- this.perf.trackedFiles = this.entries.size;
831
- this.perf.dirtyPathQueue = this.dirtyPaths.size;
832
- const retentionStats = this.buildRetentionStats();
833
- this.perf.hotTraces = retentionStats.hotTraces;
834
- this.perf.warmTraces = retentionStats.warmTraces;
835
- this.perf.coldTraces = retentionStats.coldTraces;
836
- this.perf.materializedTraces = retentionStats.materializedTraces;
837
- this.emitStream("overview_updated", { overview: this.getOverview() });
1032
+ else if (this.hasPendingHydrationWork()) {
1033
+ await this.hydratePendingBatch(refreshNowMs, stats);
1034
+ }
1035
+ if (this.startupStatus.inspectorReady && this.hasPendingHydrationWork()) {
1036
+ this.updateStartupProgress();
1037
+ }
1038
+ else if (this.startupStatus.inspectorReady && !this.startupStatus.fullReady) {
1039
+ this.startupStatus = {
1040
+ ...this.startupStatus,
1041
+ phase: "ready",
1042
+ fullReady: true,
1043
+ isPartial: false,
1044
+ hydratedTraceCount: this.startupStatus.discoveredTraceCount,
1045
+ };
1046
+ }
1047
+ this.finishMutationBatch(refreshNowMs, stats);
838
1048
  return stats;
839
1049
  }
840
1050
  async refreshFull(refreshNowMs, stats) {
841
1051
  const files = await discoverTraceFiles(this.config);
842
1052
  this.lastFullRefreshAtMs = refreshNowMs;
843
1053
  this.forceReparsePaths.clear();
1054
+ this.pendingHydrationFiles = [];
1055
+ this.pendingHydrationIds.clear();
1056
+ this.pendingHydrationPaths.clear();
1057
+ this.pendingHydrationCursor = 0;
1058
+ this.discoveredFilesById.clear();
844
1059
  const nextIds = new Set(files.map((file) => file.id));
845
1060
  const nextPathToId = new Map();
1061
+ const discoveredFilesById = new Map();
846
1062
  for (const file of files) {
847
1063
  nextPathToId.set(file.path, file.id);
1064
+ discoveredFilesById.set(file.id, file);
848
1065
  }
849
1066
  this.pathToId = nextPathToId;
1067
+ this.discoveredFilesById = discoveredFilesById;
850
1068
  for (const existingId of this.entries.keys()) {
851
1069
  if (!nextIds.has(existingId)) {
852
1070
  this.entries.delete(existingId);
@@ -861,7 +1079,17 @@ export class TraceIndex extends EventEmitter {
861
1079
  stats.parsedFileCount += 1;
862
1080
  stats.hadFileMutations = true;
863
1081
  }
1082
+ this.applyRetention(refreshNowMs);
864
1083
  }
1084
+ this.startupStatus = {
1085
+ phase: "ready",
1086
+ inspectorReady: true,
1087
+ fullReady: true,
1088
+ isPartial: false,
1089
+ discoveredTraceCount: files.length,
1090
+ hydratedTraceCount: files.length,
1091
+ startupError: "",
1092
+ };
865
1093
  }
866
1094
  async refreshDirty(refreshNowMs, stats) {
867
1095
  if (this.dirtyPaths.size === 0)
@@ -893,14 +1121,20 @@ export class TraceIndex extends EventEmitter {
893
1121
  this.emitStream("trace_removed", { id: existingId });
894
1122
  stats.hadFileMutations = true;
895
1123
  }
1124
+ this.pendingHydrationPaths.delete(normalizedPath);
1125
+ this.pendingHydrationIds.delete(existingId ?? "");
1126
+ if (existingId)
1127
+ this.discoveredFilesById.delete(existingId);
896
1128
  this.pathToId.delete(normalizedPath);
897
1129
  continue;
898
1130
  }
899
1131
  this.pathToId.set(file.path, file.id);
1132
+ this.discoveredFilesById.set(file.id, file);
900
1133
  if (existingId && existingId !== file.id && this.entries.delete(existingId)) {
901
1134
  this.cursorById.delete(existingId);
902
1135
  this.emitStream("trace_removed", { id: existingId });
903
1136
  stats.hadFileMutations = true;
1137
+ this.discoveredFilesById.delete(existingId);
904
1138
  }
905
1139
  if (processedIds.has(file.id)) {
906
1140
  if (!dirtyEntry.forceReparse)
@@ -914,6 +1148,33 @@ export class TraceIndex extends EventEmitter {
914
1148
  stats.parsedFileCount += 1;
915
1149
  stats.hadFileMutations = true;
916
1150
  }
1151
+ this.pendingHydrationIds.delete(file.id);
1152
+ this.pendingHydrationPaths.delete(file.path);
1153
+ }
1154
+ }
1155
+ async hydratePendingBatch(refreshNowMs, stats) {
1156
+ if (!this.hasPendingHydrationWork())
1157
+ return;
1158
+ let processedCount = 0;
1159
+ while (this.pendingHydrationCursor < this.pendingHydrationFiles.length && processedCount < BACKGROUND_HYDRATE_BATCH_SIZE) {
1160
+ const file = this.pendingHydrationFiles[this.pendingHydrationCursor];
1161
+ this.pendingHydrationCursor += 1;
1162
+ if (!file)
1163
+ continue;
1164
+ if (!this.pendingHydrationIds.has(file.id) || !this.pendingHydrationPaths.has(file.path))
1165
+ continue;
1166
+ this.pendingHydrationIds.delete(file.id);
1167
+ this.pendingHydrationPaths.delete(file.path);
1168
+ const changed = await this.upsertFile(file, refreshNowMs);
1169
+ if (changed) {
1170
+ stats.parsedFileCount += 1;
1171
+ stats.hadFileMutations = true;
1172
+ }
1173
+ processedCount += 1;
1174
+ }
1175
+ if (this.pendingHydrationCursor >= this.pendingHydrationFiles.length) {
1176
+ this.pendingHydrationFiles = [];
1177
+ this.pendingHydrationCursor = 0;
917
1178
  }
918
1179
  }
919
1180
  inferSourceMetadata(filePath) {
@@ -1034,6 +1295,7 @@ export class TraceIndex extends EventEmitter {
1034
1295
  const parsed = await this.parserRegistry.parseFile(file);
1035
1296
  const summary = summarize(file, parsed.agent, parsed.parser, parsed.sessionId, parsed.events, parsed.parseError, this.config, refreshNowMs);
1036
1297
  const redactedEvents = redactEvents(parsed.events, this.config.redaction);
1298
+ const activityArtifacts = buildSessionActivityArtifacts(redactedEvents);
1037
1299
  const previousCount = current?.summary.eventCount ?? 0;
1038
1300
  this.entries.set(file.id, {
1039
1301
  file,
@@ -1041,7 +1303,8 @@ export class TraceIndex extends EventEmitter {
1041
1303
  residentEvents: redactedEvents,
1042
1304
  cachedFullEvents: redactedEvents,
1043
1305
  cachedRawEvents: parsed.events,
1044
- activityArtifacts: null,
1306
+ activityArtifacts,
1307
+ usageArtifacts: buildSessionUsageArtifacts(parsed.events, parsed.agent, this.config),
1045
1308
  pinnedMaterializedAtMs: current?.pinnedMaterializedAtMs ?? 0,
1046
1309
  });
1047
1310
  this.cursorById.set(file.id, {
@@ -1071,6 +1334,7 @@ export class TraceIndex extends EventEmitter {
1071
1334
  cachedFullEvents: [],
1072
1335
  cachedRawEvents: [],
1073
1336
  activityArtifacts: null,
1337
+ usageArtifacts: null,
1074
1338
  pinnedMaterializedAtMs: 0,
1075
1339
  });
1076
1340
  this.cursorById.set(file.id, {
@@ -1141,6 +1405,7 @@ export class TraceIndex extends EventEmitter {
1141
1405
  const rebasedRawEvents = rebaseEvents(parsed.events);
1142
1406
  const mergedEvents = current.cachedFullEvents.concat(rebasedEvents);
1143
1407
  const mergedRawEvents = current.cachedRawEvents.concat(rebasedRawEvents);
1408
+ const activityArtifacts = buildSessionActivityArtifacts(mergedEvents);
1144
1409
  const mergedSummary = summarize(file, current.summary.agent, current.summary.parser, current.summary.sessionId || parsed.sessionId, mergedRawEvents, "", this.config, refreshNowMs);
1145
1410
  const summary = {
1146
1411
  ...mergedSummary,
@@ -1154,7 +1419,8 @@ export class TraceIndex extends EventEmitter {
1154
1419
  residentEvents: mergedEvents,
1155
1420
  cachedFullEvents: mergedEvents,
1156
1421
  cachedRawEvents: mergedRawEvents,
1157
- activityArtifacts: null,
1422
+ activityArtifacts,
1423
+ usageArtifacts: buildSessionUsageArtifacts(mergedRawEvents, current.summary.agent, this.config),
1158
1424
  pinnedMaterializedAtMs: current.pinnedMaterializedAtMs,
1159
1425
  });
1160
1426
  this.cursorById.set(file.id, {
@@ -1214,6 +1480,9 @@ export class TraceIndex extends EventEmitter {
1214
1480
  entry.cachedFullEvents = null;
1215
1481
  entry.cachedRawEvents = null;
1216
1482
  }
1483
+ if (this.config.retention.strategy !== "full_memory" && tier !== "hot" && entry.activityArtifacts) {
1484
+ entry.activityArtifacts = compactActivityArtifacts(entry.activityArtifacts);
1485
+ }
1217
1486
  if (this.config.retention.strategy === "full_memory" && !entry.cachedFullEvents) {
1218
1487
  entry.cachedFullEvents = sourceEvents;
1219
1488
  }
@@ -1299,7 +1568,8 @@ export class TraceIndex extends EventEmitter {
1299
1568
  const refreshedSummary = summarize(entry.file, parsed.agent, parsed.parser, parsed.sessionId, parsed.events, parsed.parseError, this.config, nowMs());
1300
1569
  entry.cachedFullEvents = redactedEvents;
1301
1570
  entry.cachedRawEvents = parsed.events;
1302
- entry.activityArtifacts = null;
1571
+ entry.activityArtifacts = buildSessionActivityArtifacts(redactedEvents);
1572
+ entry.usageArtifacts = buildSessionUsageArtifacts(parsed.events, parsed.agent, this.config);
1303
1573
  entry.pinnedMaterializedAtMs = nowMs();
1304
1574
  entry.summary = {
1305
1575
  ...refreshedSummary,
@@ -1327,9 +1597,40 @@ export class TraceIndex extends EventEmitter {
1327
1597
  if (found.activityArtifacts && found.activityArtifacts.eventCount === found.summary.eventCount) {
1328
1598
  return found.activityArtifacts;
1329
1599
  }
1330
- const events = this.hydrateEventsForEntry(found);
1331
- const artifacts = buildSessionActivityArtifacts(events);
1332
- found.activityArtifacts = artifacts;
1600
+ const artifacts = (() => {
1601
+ if (found.cachedFullEvents && found.cachedFullEvents.length >= found.summary.eventCount) {
1602
+ return buildSessionActivityArtifacts(found.cachedFullEvents);
1603
+ }
1604
+ if (found.residentEvents.length >= found.summary.eventCount) {
1605
+ return buildSessionActivityArtifacts(found.residentEvents);
1606
+ }
1607
+ const parsed = this.parserRegistry.parseFileSync(found.file, found.summary.parser);
1608
+ const redactedEvents = redactEvents(parsed.events, this.config.redaction);
1609
+ return buildSessionActivityArtifacts(redactedEvents);
1610
+ })();
1611
+ if (this.config.retention.strategy === "full_memory" || found.summary.residentTier === "hot") {
1612
+ found.activityArtifacts = artifacts;
1613
+ }
1614
+ return artifacts;
1615
+ }
1616
+ getSessionUsageArtifacts(id) {
1617
+ const found = this.entries.get(id);
1618
+ if (!found) {
1619
+ throw new Error(`unknown trace id: ${id}`);
1620
+ }
1621
+ if (found.usageArtifacts && found.usageArtifacts.eventCount === found.summary.eventCount) {
1622
+ return found.usageArtifacts;
1623
+ }
1624
+ const artifacts = (() => {
1625
+ if (found.cachedRawEvents && found.cachedRawEvents.length >= found.summary.eventCount) {
1626
+ return buildSessionUsageArtifacts(found.cachedRawEvents, found.summary.agent, this.config);
1627
+ }
1628
+ const parsed = this.parserRegistry.parseFileSync(found.file, found.summary.parser);
1629
+ return buildSessionUsageArtifacts(parsed.events, parsed.agent, this.config);
1630
+ })();
1631
+ if (this.config.retention.strategy === "full_memory" || found.summary.residentTier === "hot") {
1632
+ found.usageArtifacts = artifacts;
1633
+ }
1333
1634
  return artifacts;
1334
1635
  }
1335
1636
  getTracePage(id, options = {}) {