agentfootprint-lens 0.20.0 → 0.21.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.
@@ -306,6 +306,7 @@ function relTime(runStartMs) {
306
306
  }
307
307
 
308
308
  // src/core/LensRecorder.ts
309
+ var DEFAULT_MAX_EVENTS = 5e4;
309
310
  var KNOWN_EVENT_TYPES = new Set(ALL_EVENT_TYPES);
310
311
  var LensRecorder = class {
311
312
  constructor(rootLabel = "Run", options = {}) {
@@ -387,7 +388,18 @@ var LensRecorder = class {
387
388
  /** Unknown types already warned about — warn ONCE per type, not per
388
389
  * event, so a chatty unknown emitter can't flood the console. */
389
390
  this.warnedUnknownTypes = /* @__PURE__ */ new Set();
391
+ /** Entries evicted by the cap so far. Surfaced via `getDiagnostics()`. */
392
+ this.droppedEventCount = 0;
393
+ /** Eviction already warned about — warn ONCE per run, not per batch. */
394
+ this.warnedEviction = false;
390
395
  this.debug = options.debug;
396
+ const cap = options.maxEvents ?? DEFAULT_MAX_EVENTS;
397
+ if (cap !== Number.POSITIVE_INFINITY && (!Number.isInteger(cap) || cap < 1)) {
398
+ throw new RangeError(
399
+ `LensRecorder: maxEvents must be a positive integer or Infinity, got ${cap}`
400
+ );
401
+ }
402
+ this.maxEvents = cap;
391
403
  this.root = {
392
404
  id: "run-root",
393
405
  kind: "run",
@@ -417,6 +429,8 @@ var LensRecorder = class {
417
429
  this.unknownEventTypes.clear();
418
430
  this.bracketMismatchCount = 0;
419
431
  this.warnedUnknownTypes.clear();
432
+ this.droppedEventCount = 0;
433
+ this.warnedEviction = false;
420
434
  this.liveState.clear();
421
435
  this.boundary.clear();
422
436
  this.runtime.reset();
@@ -436,14 +450,18 @@ var LensRecorder = class {
436
450
  * `composition.exit`, ...) whose kind didn't match the top of the
437
451
  * build stack (malformed ordering). The close is skipped; the
438
452
  * tree stays partially structured rather than crashing.
453
+ * - `droppedEvents` — entries evicted by the `maxEvents` FIFO cap
454
+ * (U3). When non-zero, log-derived views cover only the retained
455
+ * tail of the run.
439
456
  *
440
- * Both are `{}` / `0` on a well-formed run. Reset by `clear()`.
441
- * Returns a fresh snapshot object on every call.
457
+ * All are `{}` / `0` on a well-formed run that stayed under the cap.
458
+ * Reset by `clear()`. Returns a fresh snapshot object on every call.
442
459
  */
443
460
  getDiagnostics() {
444
461
  return {
445
462
  unknownEventTypes: Object.fromEntries(this.unknownEventTypes),
446
- bracketMismatches: this.bracketMismatchCount
463
+ bracketMismatches: this.bracketMismatchCount,
464
+ droppedEvents: this.droppedEventCount
447
465
  };
448
466
  }
449
467
  /** Whether diagnostic warnings go to the console: explicit option
@@ -604,8 +622,52 @@ var LensRecorder = class {
604
622
  this.top().events.push(entry);
605
623
  this.noteUnknownType(event.type);
606
624
  this.dispatch(event, runOffsetMs, entry);
625
+ this.enforceCap();
607
626
  this.bumpVersion();
608
627
  }
628
+ /**
629
+ * U3 — enforce the `maxEvents` FIFO cap. When the store exceeds the
630
+ * cap, evict the oldest entries down to ~90% of the cap in ONE batch
631
+ * (amortized O(1) per event: one O(retained) rebuild per ~10%-of-cap
632
+ * pushes), then prune the SAME evicted entries from every run-tree
633
+ * node's `events` list — entry objects are shared references, so
634
+ * skipping the tree would hide the memory, not release it.
635
+ *
636
+ * `SequenceStore` is append-only by design (no removal API), so the
637
+ * batch rebuild (clear + re-push retained) is the supported eviction
638
+ * path; the per-step key + range indices rebuild correctly during
639
+ * re-push.
640
+ */
641
+ enforceCap() {
642
+ if (this.store.size <= this.maxEvents) return;
643
+ const all = this.store.getAll();
644
+ const evictBatch = Math.max(1, Math.floor(this.maxEvents / 10));
645
+ const retainCount = Math.max(1, this.maxEvents - evictBatch);
646
+ const dropCount = all.length - retainCount;
647
+ const retained = all.slice(dropCount);
648
+ this.store.clear();
649
+ for (const e of retained) this.store.push(e);
650
+ this.droppedEventCount += dropCount;
651
+ this.pruneNodeEvents(this.root, retained[0].seq);
652
+ if (this.debugEnabled() && !this.warnedEviction) {
653
+ this.warnedEviction = true;
654
+ console.warn(
655
+ `[lens] LensRecorder: maxEvents cap (${this.maxEvents}) reached \u2014 evicting oldest events (FIFO, ~10% per batch). Log-derived views now cover only the retained tail; evicted total in getDiagnostics().droppedEvents. Raise the cap via lensRecorder('Run', { maxEvents }) or pass Infinity to disable. (Warned once.)`
656
+ );
657
+ }
658
+ }
659
+ /** Drop entries with `seq < minSeq` from a node's `events` list (and
660
+ * its descendants'). Per-node lists are seq-ordered, so this is a
661
+ * prefix splice — in place, preserving the node object identity the
662
+ * build stack may still hold. */
663
+ pruneNodeEvents(node, minSeq) {
664
+ if (node.events.length > 0 && node.events[0].seq < minSeq) {
665
+ let keepFrom = 0;
666
+ while (keepFrom < node.events.length && node.events[keepFrom].seq < minSeq) keepFrom++;
667
+ node.events.splice(0, keepFrom);
668
+ }
669
+ for (const child of node.children) this.pruneNodeEvents(child, minSeq);
670
+ }
609
671
  /** Notify all subscribers + bump version. Delegated to ChangeNotifier. */
610
672
  bumpVersion() {
611
673
  this.notifier.notify();
@@ -904,7 +966,13 @@ var LensRecorder = class {
904
966
  }
905
967
  /** Summary stats — computed lazily via `store.aggregate()`.
906
968
  * Single-pass fold; types derived from the AgentfootprintEvent
907
- * discriminated union. */
969
+ * discriminated union.
970
+ *
971
+ * U3 caveat: once the `maxEvents` cap has evicted entries
972
+ * (`getDiagnostics().droppedEvents > 0`), the folded counts/tokens
973
+ * reflect only RETAINED events. `startedAt` / `durationMs` stay
974
+ * anchored to the true first event of the run (tracked outside the
975
+ * store), so the time axis never shifts. */
908
976
  selectSummary() {
909
977
  const init = {
910
978
  llmCallCount: 0,
@@ -944,7 +1012,7 @@ var LensRecorder = class {
944
1012
  }
945
1013
  }, init);
946
1014
  const entries = this.store.getAll();
947
- const startedAt = entries[0]?.wallClockMs ?? 0;
1015
+ const startedAt = this.runStartMs ?? entries[0]?.wallClockMs ?? 0;
948
1016
  const endedAt = entries[entries.length - 1]?.wallClockMs;
949
1017
  return {
950
1018
  startedAt,
@@ -2348,6 +2416,7 @@ export {
2348
2416
  ChangeNotifier,
2349
2417
  lensSnapshotRecorder,
2350
2418
  LensSnapshotRecorder,
2419
+ DEFAULT_MAX_EVENTS,
2351
2420
  LensRecorder,
2352
2421
  lensRecorder,
2353
2422
  buildStepGraphFromSnapshot,
@@ -2385,4 +2454,4 @@ export {
2385
2454
  defaultSize,
2386
2455
  layoutLensGraph
2387
2456
  };
2388
- //# sourceMappingURL=chunk-N3YJKWK5.js.map
2457
+ //# sourceMappingURL=chunk-BTVAAE66.js.map