@roberttlange/agentlens 0.2.2 → 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 (48) 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 +120 -0
  6. package/node_modules/@agentlens/core/dist/__tests__/config.test.js +67 -2
  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 +590 -2
  9. package/node_modules/@agentlens/core/dist/__tests__/index.test.js.map +1 -1
  10. package/node_modules/@agentlens/core/dist/config.js +95 -5
  11. package/node_modules/@agentlens/core/dist/config.js.map +1 -1
  12. package/node_modules/@agentlens/core/dist/generatedPricing.d.ts +3 -0
  13. package/node_modules/@agentlens/core/dist/generatedPricing.js +131 -0
  14. package/node_modules/@agentlens/core/dist/generatedPricing.js.map +1 -0
  15. package/node_modules/@agentlens/core/dist/metrics.d.ts +13 -0
  16. package/node_modules/@agentlens/core/dist/metrics.js +227 -54
  17. package/node_modules/@agentlens/core/dist/metrics.js.map +1 -1
  18. package/node_modules/@agentlens/core/dist/pricing.d.ts +15 -0
  19. package/node_modules/@agentlens/core/dist/pricing.js +133 -0
  20. package/node_modules/@agentlens/core/dist/pricing.js.map +1 -0
  21. package/node_modules/@agentlens/core/dist/pricing.test.d.ts +1 -0
  22. package/node_modules/@agentlens/core/dist/pricing.test.js +109 -0
  23. package/node_modules/@agentlens/core/dist/pricing.test.js.map +1 -0
  24. package/node_modules/@agentlens/core/dist/sourceProfiles.js +7 -67
  25. package/node_modules/@agentlens/core/dist/sourceProfiles.js.map +1 -1
  26. package/node_modules/@agentlens/core/dist/traceIndex.d.ts +34 -1
  27. package/node_modules/@agentlens/core/dist/traceIndex.js +374 -15
  28. package/node_modules/@agentlens/core/dist/traceIndex.js.map +1 -1
  29. package/node_modules/@agentlens/server/dist/activity-cache.d.ts +32 -0
  30. package/node_modules/@agentlens/server/dist/activity-cache.js +63 -0
  31. package/node_modules/@agentlens/server/dist/activity-cache.js.map +1 -0
  32. package/node_modules/@agentlens/server/dist/activity-cache.test.d.ts +1 -0
  33. package/node_modules/@agentlens/server/dist/activity-cache.test.js +170 -0
  34. package/node_modules/@agentlens/server/dist/activity-cache.test.js.map +1 -0
  35. package/node_modules/@agentlens/server/dist/activity.d.ts +31 -1
  36. package/node_modules/@agentlens/server/dist/activity.js +532 -34
  37. package/node_modules/@agentlens/server/dist/activity.js.map +1 -1
  38. package/node_modules/@agentlens/server/dist/app.d.ts +4 -2
  39. package/node_modules/@agentlens/server/dist/app.js +248 -5
  40. package/node_modules/@agentlens/server/dist/app.js.map +1 -1
  41. package/node_modules/@agentlens/server/dist/app.test.js +670 -9
  42. package/node_modules/@agentlens/server/dist/app.test.js.map +1 -1
  43. package/node_modules/@agentlens/server/dist/web/assets/index-CTFOBaBt.css +1 -0
  44. package/node_modules/@agentlens/server/dist/web/assets/index-CVf00w06.js +52 -0
  45. package/node_modules/@agentlens/server/dist/web/index.html +2 -2
  46. package/package.json +1 -1
  47. package/node_modules/@agentlens/server/dist/web/assets/index-Ci8okH8M.js +0 -52
  48. package/node_modules/@agentlens/server/dist/web/assets/index-Cj3kmsFf.css +0 -1
@@ -22,8 +22,27 @@ const EVENT_KIND_KEYS = [
22
22
  const WAITING_INPUT_PATTERN = /\b(?:await(?:ing)?\s+(?:user|input)|waiting\s+for\s+(?:user|input|approval)|user\s+input\s+required|needs?\s+user\s+input|permission\s+required|approval\s+required|confirm(?:ation)?\s+(?:required|needed)|press\s+enter\s+to\s+continue)\b/i;
23
23
  const WAITING_PROMPT_PATTERN = /\b(?:do\s+you\s+want(?:\s+me)?|would\s+you\s+like(?:\s+me)?|should\s+i\b|can\s+you\s+confirm|please\s+confirm|let\s+me\s+know\s+if\s+you(?:'d)?\s+like|which\s+(?:option|approach)|choose\s+(?:one|an?\s+option)|pick\s+(?:one|an?\s+option)|approve(?:\s+this)?|permission\s+to)\b/i;
24
24
  const ACTIVITY_BIN_COUNT = 12;
25
+ const ACTIVE_IDLE_GAP_MS = 20 * 60_000;
25
26
  const MATERIALIZED_TTL_MS = 5 * 60_000;
26
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
+ }
27
46
  function emptyEventKindCounts() {
28
47
  return {
29
48
  system: 0,
@@ -36,6 +55,58 @@ function emptyEventKindCounts() {
36
55
  meta: 0,
37
56
  };
38
57
  }
58
+ function collectTimestampMs(events) {
59
+ const timestamps = [];
60
+ for (const event of events) {
61
+ const timestampMs = event.timestampMs;
62
+ if (timestampMs === null || !Number.isFinite(timestampMs) || timestampMs <= 0)
63
+ continue;
64
+ timestamps.push(timestampMs);
65
+ }
66
+ timestamps.sort((left, right) => left - right);
67
+ return timestamps;
68
+ }
69
+ function buildActiveSegmentsFromEventTimestamps(eventTimestamps) {
70
+ if (eventTimestamps.length === 0)
71
+ return [];
72
+ const segments = [];
73
+ let segmentStartMs = eventTimestamps[0] ?? 0;
74
+ let previousTsMs = segmentStartMs;
75
+ for (let index = 1; index < eventTimestamps.length; index += 1) {
76
+ const nextTsMs = eventTimestamps[index] ?? previousTsMs;
77
+ const gapMs = Math.max(0, nextTsMs - previousTsMs);
78
+ if (gapMs > ACTIVE_IDLE_GAP_MS) {
79
+ segments.push({ startMs: segmentStartMs, endMs: previousTsMs });
80
+ segmentStartMs = nextTsMs;
81
+ }
82
+ previousTsMs = nextTsMs;
83
+ }
84
+ segments.push({ startMs: segmentStartMs, endMs: previousTsMs });
85
+ return segments;
86
+ }
87
+ function buildSessionActivityArtifacts(events) {
88
+ const eventTimestamps = collectTimestampMs(events);
89
+ return {
90
+ eventCount: events.length,
91
+ eventTimestamps,
92
+ activeSegments: buildActiveSegmentsFromEventTimestamps(eventTimestamps),
93
+ };
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
+ }
39
110
  function normalizeActivityCounts(counts) {
40
111
  let maxCount = 0;
41
112
  for (const count of counts) {
@@ -441,6 +512,7 @@ export class TraceIndex extends EventEmitter {
441
512
  config;
442
513
  entries = new Map();
443
514
  pathToId = new Map();
515
+ discoveredFilesById = new Map();
444
516
  cursorById = new Map();
445
517
  watcher = null;
446
518
  timer = null;
@@ -451,6 +523,11 @@ export class TraceIndex extends EventEmitter {
451
523
  queuedForceFullRefresh = false;
452
524
  dirtyPaths = new Set();
453
525
  forceReparsePaths = new Set();
526
+ pendingHydrationFiles = [];
527
+ pendingHydrationIds = new Set();
528
+ pendingHydrationPaths = new Set();
529
+ pendingHydrationCursor = 0;
530
+ startupStatus = createInitialStartupStatus();
454
531
  adaptiveIntervalMs;
455
532
  perf = {
456
533
  refreshCount: 0,
@@ -494,8 +571,43 @@ export class TraceIndex extends EventEmitter {
494
571
  }
495
572
  async start() {
496
573
  this.started = true;
497
- 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;
498
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;
499
611
  this.scheduleNextRefresh(this.computeBaseIntervalMs());
500
612
  }
501
613
  stop() {
@@ -509,6 +621,11 @@ export class TraceIndex extends EventEmitter {
509
621
  this.watcher = null;
510
622
  }
511
623
  this.forceReparsePaths.clear();
624
+ this.pendingHydrationFiles = [];
625
+ this.pendingHydrationIds.clear();
626
+ this.pendingHydrationPaths.clear();
627
+ this.pendingHydrationCursor = 0;
628
+ this.discoveredFilesById.clear();
512
629
  }
513
630
  async refresh() {
514
631
  this.queuedForceFullRefresh = true;
@@ -523,6 +640,43 @@ export class TraceIndex extends EventEmitter {
523
640
  ...stats,
524
641
  };
525
642
  }
643
+ getStreamVersion() {
644
+ return this.streamVersion;
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
+ }
526
680
  getTopTools(limit = 12) {
527
681
  const counts = new Map();
528
682
  for (const entry of this.entries.values()) {
@@ -535,6 +689,91 @@ export class TraceIndex extends EventEmitter {
535
689
  .slice(0, Math.max(1, limit))
536
690
  .map(([name, count]) => ({ name, count }));
537
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
+ }
538
777
  buildRetentionStats() {
539
778
  let hotTraces = 0;
540
779
  let warmTraces = 0;
@@ -584,13 +823,14 @@ export class TraceIndex extends EventEmitter {
584
823
  if (roots.length === 0)
585
824
  return;
586
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));
587
827
  this.watcher = chokidar.watch(roots, {
588
828
  ignoreInitial: true,
589
829
  persistent: true,
590
830
  followSymlinks: false,
591
831
  awaitWriteFinish: {
592
- stabilityThreshold: debounceMs,
593
- pollInterval: 40,
832
+ stabilityThreshold: writeStabilityMs,
833
+ pollInterval: WATCH_WRITE_POLL_INTERVAL_MS,
594
834
  },
595
835
  });
596
836
  const onDirty = (rawPath) => {
@@ -610,7 +850,7 @@ export class TraceIndex extends EventEmitter {
610
850
  }
611
851
  }
612
852
  this.perf.dirtyPathQueue = this.dirtyPaths.size;
613
- this.scheduleNextRefresh(debounceMs);
853
+ this.scheduleNextRefresh(DIRTY_REFRESH_DELAY_MS);
614
854
  };
615
855
  this.watcher.on("add", onDirty);
616
856
  this.watcher.on("change", onDirty);
@@ -711,6 +951,8 @@ export class TraceIndex extends EventEmitter {
711
951
  return Array.from(matches.values());
712
952
  }
713
953
  shouldRunFullRefresh(nowMsValue) {
954
+ if (this.hasPendingHydrationWork())
955
+ return false;
714
956
  if (this.lastFullRefreshAtMs <= 0)
715
957
  return true;
716
958
  const fullIntervalMs = Math.max(1_000, this.config.scan.fullRescanIntervalMs);
@@ -740,6 +982,9 @@ export class TraceIndex extends EventEmitter {
740
982
  if (this.refreshPending || this.queuedForceFullRefresh) {
741
983
  this.scheduleNextRefresh(25);
742
984
  }
985
+ else if (this.hasPendingHydrationWork()) {
986
+ this.scheduleNextRefresh(BACKGROUND_HYDRATE_DELAY_MS);
987
+ }
743
988
  else if (this.started) {
744
989
  this.scheduleNextRefresh();
745
990
  }
@@ -781,31 +1026,45 @@ export class TraceIndex extends EventEmitter {
781
1026
  if (useFullRefresh) {
782
1027
  await this.refreshFull(refreshNowMs, stats);
783
1028
  }
784
- else {
1029
+ else if (this.dirtyPaths.size > 0) {
785
1030
  await this.refreshDirty(refreshNowMs, stats);
786
1031
  }
787
- this.applyRetention(refreshNowMs);
788
- this.refreshActivityStatus(refreshNowMs, stats);
789
- this.perf.trackedFiles = this.entries.size;
790
- this.perf.dirtyPathQueue = this.dirtyPaths.size;
791
- const retentionStats = this.buildRetentionStats();
792
- this.perf.hotTraces = retentionStats.hotTraces;
793
- this.perf.warmTraces = retentionStats.warmTraces;
794
- this.perf.coldTraces = retentionStats.coldTraces;
795
- this.perf.materializedTraces = retentionStats.materializedTraces;
796
- 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);
797
1048
  return stats;
798
1049
  }
799
1050
  async refreshFull(refreshNowMs, stats) {
800
1051
  const files = await discoverTraceFiles(this.config);
801
1052
  this.lastFullRefreshAtMs = refreshNowMs;
802
1053
  this.forceReparsePaths.clear();
1054
+ this.pendingHydrationFiles = [];
1055
+ this.pendingHydrationIds.clear();
1056
+ this.pendingHydrationPaths.clear();
1057
+ this.pendingHydrationCursor = 0;
1058
+ this.discoveredFilesById.clear();
803
1059
  const nextIds = new Set(files.map((file) => file.id));
804
1060
  const nextPathToId = new Map();
1061
+ const discoveredFilesById = new Map();
805
1062
  for (const file of files) {
806
1063
  nextPathToId.set(file.path, file.id);
1064
+ discoveredFilesById.set(file.id, file);
807
1065
  }
808
1066
  this.pathToId = nextPathToId;
1067
+ this.discoveredFilesById = discoveredFilesById;
809
1068
  for (const existingId of this.entries.keys()) {
810
1069
  if (!nextIds.has(existingId)) {
811
1070
  this.entries.delete(existingId);
@@ -820,7 +1079,17 @@ export class TraceIndex extends EventEmitter {
820
1079
  stats.parsedFileCount += 1;
821
1080
  stats.hadFileMutations = true;
822
1081
  }
1082
+ this.applyRetention(refreshNowMs);
823
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
+ };
824
1093
  }
825
1094
  async refreshDirty(refreshNowMs, stats) {
826
1095
  if (this.dirtyPaths.size === 0)
@@ -852,14 +1121,20 @@ export class TraceIndex extends EventEmitter {
852
1121
  this.emitStream("trace_removed", { id: existingId });
853
1122
  stats.hadFileMutations = true;
854
1123
  }
1124
+ this.pendingHydrationPaths.delete(normalizedPath);
1125
+ this.pendingHydrationIds.delete(existingId ?? "");
1126
+ if (existingId)
1127
+ this.discoveredFilesById.delete(existingId);
855
1128
  this.pathToId.delete(normalizedPath);
856
1129
  continue;
857
1130
  }
858
1131
  this.pathToId.set(file.path, file.id);
1132
+ this.discoveredFilesById.set(file.id, file);
859
1133
  if (existingId && existingId !== file.id && this.entries.delete(existingId)) {
860
1134
  this.cursorById.delete(existingId);
861
1135
  this.emitStream("trace_removed", { id: existingId });
862
1136
  stats.hadFileMutations = true;
1137
+ this.discoveredFilesById.delete(existingId);
863
1138
  }
864
1139
  if (processedIds.has(file.id)) {
865
1140
  if (!dirtyEntry.forceReparse)
@@ -873,6 +1148,33 @@ export class TraceIndex extends EventEmitter {
873
1148
  stats.parsedFileCount += 1;
874
1149
  stats.hadFileMutations = true;
875
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;
876
1178
  }
877
1179
  }
878
1180
  inferSourceMetadata(filePath) {
@@ -993,6 +1295,7 @@ export class TraceIndex extends EventEmitter {
993
1295
  const parsed = await this.parserRegistry.parseFile(file);
994
1296
  const summary = summarize(file, parsed.agent, parsed.parser, parsed.sessionId, parsed.events, parsed.parseError, this.config, refreshNowMs);
995
1297
  const redactedEvents = redactEvents(parsed.events, this.config.redaction);
1298
+ const activityArtifacts = buildSessionActivityArtifacts(redactedEvents);
996
1299
  const previousCount = current?.summary.eventCount ?? 0;
997
1300
  this.entries.set(file.id, {
998
1301
  file,
@@ -1000,6 +1303,8 @@ export class TraceIndex extends EventEmitter {
1000
1303
  residentEvents: redactedEvents,
1001
1304
  cachedFullEvents: redactedEvents,
1002
1305
  cachedRawEvents: parsed.events,
1306
+ activityArtifacts,
1307
+ usageArtifacts: buildSessionUsageArtifacts(parsed.events, parsed.agent, this.config),
1003
1308
  pinnedMaterializedAtMs: current?.pinnedMaterializedAtMs ?? 0,
1004
1309
  });
1005
1310
  this.cursorById.set(file.id, {
@@ -1028,6 +1333,8 @@ export class TraceIndex extends EventEmitter {
1028
1333
  residentEvents: [],
1029
1334
  cachedFullEvents: [],
1030
1335
  cachedRawEvents: [],
1336
+ activityArtifacts: null,
1337
+ usageArtifacts: null,
1031
1338
  pinnedMaterializedAtMs: 0,
1032
1339
  });
1033
1340
  this.cursorById.set(file.id, {
@@ -1098,6 +1405,7 @@ export class TraceIndex extends EventEmitter {
1098
1405
  const rebasedRawEvents = rebaseEvents(parsed.events);
1099
1406
  const mergedEvents = current.cachedFullEvents.concat(rebasedEvents);
1100
1407
  const mergedRawEvents = current.cachedRawEvents.concat(rebasedRawEvents);
1408
+ const activityArtifacts = buildSessionActivityArtifacts(mergedEvents);
1101
1409
  const mergedSummary = summarize(file, current.summary.agent, current.summary.parser, current.summary.sessionId || parsed.sessionId, mergedRawEvents, "", this.config, refreshNowMs);
1102
1410
  const summary = {
1103
1411
  ...mergedSummary,
@@ -1111,6 +1419,8 @@ export class TraceIndex extends EventEmitter {
1111
1419
  residentEvents: mergedEvents,
1112
1420
  cachedFullEvents: mergedEvents,
1113
1421
  cachedRawEvents: mergedRawEvents,
1422
+ activityArtifacts,
1423
+ usageArtifacts: buildSessionUsageArtifacts(mergedRawEvents, current.summary.agent, this.config),
1114
1424
  pinnedMaterializedAtMs: current.pinnedMaterializedAtMs,
1115
1425
  });
1116
1426
  this.cursorById.set(file.id, {
@@ -1170,6 +1480,9 @@ export class TraceIndex extends EventEmitter {
1170
1480
  entry.cachedFullEvents = null;
1171
1481
  entry.cachedRawEvents = null;
1172
1482
  }
1483
+ if (this.config.retention.strategy !== "full_memory" && tier !== "hot" && entry.activityArtifacts) {
1484
+ entry.activityArtifacts = compactActivityArtifacts(entry.activityArtifacts);
1485
+ }
1173
1486
  if (this.config.retention.strategy === "full_memory" && !entry.cachedFullEvents) {
1174
1487
  entry.cachedFullEvents = sourceEvents;
1175
1488
  }
@@ -1255,6 +1568,8 @@ export class TraceIndex extends EventEmitter {
1255
1568
  const refreshedSummary = summarize(entry.file, parsed.agent, parsed.parser, parsed.sessionId, parsed.events, parsed.parseError, this.config, nowMs());
1256
1569
  entry.cachedFullEvents = redactedEvents;
1257
1570
  entry.cachedRawEvents = parsed.events;
1571
+ entry.activityArtifacts = buildSessionActivityArtifacts(redactedEvents);
1572
+ entry.usageArtifacts = buildSessionUsageArtifacts(parsed.events, parsed.agent, this.config);
1258
1573
  entry.pinnedMaterializedAtMs = nowMs();
1259
1574
  entry.summary = {
1260
1575
  ...refreshedSummary,
@@ -1274,6 +1589,50 @@ export class TraceIndex extends EventEmitter {
1274
1589
  events,
1275
1590
  };
1276
1591
  }
1592
+ getSessionActivityArtifacts(id) {
1593
+ const found = this.entries.get(id);
1594
+ if (!found) {
1595
+ throw new Error(`unknown trace id: ${id}`);
1596
+ }
1597
+ if (found.activityArtifacts && found.activityArtifacts.eventCount === found.summary.eventCount) {
1598
+ return found.activityArtifacts;
1599
+ }
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
+ }
1634
+ return artifacts;
1635
+ }
1277
1636
  getTracePage(id, options = {}) {
1278
1637
  const detail = this.getSessionDetail(id);
1279
1638
  const includeMeta = options.includeMeta ?? this.config.scan.includeMetaDefault;