@tt-a1i/openpi 0.4.0 → 0.5.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 (109) hide show
  1. package/README.md +102 -40
  2. package/SETUP.md +22 -6
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/background-terminals/index.ts +30 -2
  6. package/extensions/background-terminals/src/domain.ts +2 -0
  7. package/extensions/background-terminals/src/manager.ts +486 -106
  8. package/extensions/background-terminals/src/output.ts +33 -0
  9. package/extensions/background-terminals/src/prompt.ts +13 -5
  10. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  11. package/extensions/clear-context/index.ts +83 -0
  12. package/extensions/context-pivot/index.ts +16 -6
  13. package/extensions/cron/schedule.ts +7 -1
  14. package/extensions/file-mutation-display/render.ts +17 -257
  15. package/extensions/file-search/src/binaries.ts +57 -41
  16. package/extensions/git-read/index.ts +1 -3
  17. package/extensions/model-info/index.ts +21 -33
  18. package/extensions/model-info/session-metrics.ts +96 -0
  19. package/extensions/plan-mode/bash-policy.ts +54 -9
  20. package/extensions/plan-mode/index.ts +7 -2
  21. package/extensions/post-edit/index.ts +16 -6
  22. package/extensions/sessions/git-stats.ts +258 -72
  23. package/extensions/sessions/index.ts +153 -86
  24. package/extensions/sessions/preview-cache.ts +104 -0
  25. package/extensions/sessions/preview-loader.ts +856 -0
  26. package/extensions/sessions/sessions.ts +43 -4
  27. package/extensions/setup/index.ts +123 -127
  28. package/extensions/shared/activity-status.ts +30 -0
  29. package/extensions/shared/agent-session-page.ts +319 -0
  30. package/extensions/shared/agent-tool-renderer.ts +218 -0
  31. package/extensions/shared/agent-transcript.ts +524 -0
  32. package/extensions/shared/capability-intent.ts +1 -1
  33. package/extensions/shared/child-session.ts +437 -21
  34. package/extensions/shared/result-delivery.ts +34 -0
  35. package/extensions/shared/setup-config.ts +73 -33
  36. package/extensions/shared/setup-episode-state.ts +1 -1
  37. package/extensions/shared/terminal-text.ts +110 -23
  38. package/extensions/shared/text-projection.ts +72 -15
  39. package/extensions/shared/tool-activity.ts +382 -0
  40. package/extensions/shared/tool-surface.ts +29 -2
  41. package/extensions/shared/transcript-viewport.ts +46 -0
  42. package/extensions/shared/web-observer-registry.ts +390 -0
  43. package/extensions/shared/worktree.ts +11 -0
  44. package/extensions/subagents/index.ts +270 -59
  45. package/extensions/subagents/navigation.ts +34 -5
  46. package/extensions/subagents/src/backend.ts +12 -1
  47. package/extensions/subagents/src/backends/pi.ts +375 -66
  48. package/extensions/subagents/src/domain.ts +5 -0
  49. package/extensions/subagents/src/manager.ts +34 -2
  50. package/extensions/subagents/src/prompt.ts +32 -4
  51. package/extensions/subagents/src/result-artifact.ts +4 -0
  52. package/extensions/subagents/src/result-delivery.ts +7 -1
  53. package/extensions/subagents/src/runtime.ts +15 -1
  54. package/extensions/subagents/src/ui/takeover.ts +73 -257
  55. package/extensions/subagents/src/ui/transcript.ts +38 -535
  56. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  57. package/extensions/suggestions/src/ui.ts +10 -4
  58. package/extensions/tasks/index.ts +0 -3
  59. package/extensions/ui-customization/footer.ts +0 -40
  60. package/extensions/ui-customization/index.ts +0 -4
  61. package/extensions/user-input-fold/index.ts +1 -1
  62. package/extensions/web/index.ts +234 -0
  63. package/extensions/workflows/artifacts.ts +137 -47
  64. package/extensions/workflows/completion-projection.ts +457 -0
  65. package/extensions/workflows/coordinator.ts +8 -10
  66. package/extensions/workflows/dashboard.ts +167 -228
  67. package/extensions/workflows/handoff.ts +70 -16
  68. package/extensions/workflows/index.ts +488 -198
  69. package/extensions/workflows/journal.ts +148 -13
  70. package/extensions/workflows/model.ts +74 -4
  71. package/extensions/workflows/navigation.ts +32 -8
  72. package/extensions/workflows/progress-projection.ts +306 -0
  73. package/extensions/workflows/prompt.ts +66 -6
  74. package/extensions/workflows/replay-safety.ts +42 -21
  75. package/extensions/workflows/result-delivery.ts +128 -64
  76. package/extensions/workflows/retention.ts +593 -0
  77. package/extensions/workflows/runner.ts +388 -279
  78. package/extensions/workflows/sandbox-child.cjs +25 -3
  79. package/extensions/workflows/sandbox.ts +62 -8
  80. package/extensions/workflows/serialization.ts +325 -17
  81. package/extensions/workflows/tool-renderer.ts +22 -0
  82. package/extensions/workflows/transcript.ts +149 -0
  83. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  84. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  85. package/package.json +28 -8
  86. package/skills/subagents/REFERENCE.md +189 -0
  87. package/skills/subagents/SKILL.md +1 -1
  88. package/skills/workflows/REFERENCE.md +4 -2
  89. package/web/adapter/pi-adapter.ts +661 -0
  90. package/web/host/browser-launcher.ts +20 -0
  91. package/web/host/static-assets.ts +4 -0
  92. package/web/host/terminal-status.ts +38 -0
  93. package/web/host/web-host.ts +789 -0
  94. package/web/http-dispatcher.ts +125 -0
  95. package/web/protocol/types.ts +462 -0
  96. package/web/runtime/pi-runtime.ts +991 -0
  97. package/web/runtime/types.ts +71 -0
  98. package/web/runtime/web-host-lease.ts +497 -0
  99. package/web/trace.ts +18 -0
  100. package/web/ui/app.js +1398 -0
  101. package/web/ui/index.html +139 -0
  102. package/web/ui/styles.css +598 -0
  103. package/web/vite.config.mjs +34 -0
  104. package/extensions/execution-convergence/active-evidence.ts +0 -129
  105. package/extensions/execution-convergence/index.ts +0 -442
  106. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  107. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  108. package/extensions/setup/intercom.ts +0 -603
  109. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -22,7 +22,19 @@ import {
22
22
  wrapTextWithAnsi,
23
23
  } from "@earendil-works/pi-tui";
24
24
  import { hintLine } from "../shared/screen-chrome.ts";
25
- import { createSessionStatsLoader, type SessionStats } from "./git-stats.js";
25
+ import {
26
+ createSessionStatsLoader,
27
+ type SessionStatsState,
28
+ } from "./git-stats.js";
29
+ import {
30
+ createSessionPreviewCache,
31
+ measureSessionPreviewBytes,
32
+ previewCacheKey,
33
+ } from "./preview-cache.js";
34
+ import {
35
+ loadSessionPreviewData,
36
+ readPreviewFileIdentity,
37
+ } from "./preview-loader.js";
26
38
  import {
27
39
  buildPreviewError,
28
40
  buildSessionDescription,
@@ -33,7 +45,9 @@ import {
33
45
  formatRelativeTime,
34
46
  getSessionPaneLayout,
35
47
  type PreviewBlock,
48
+ type PreviewMessageLike,
36
49
  parseLimit,
50
+ selectSessionStatsWindow,
37
51
  type SessionInfoLike,
38
52
  type SessionPreview,
39
53
  } from "./sessions.js";
@@ -291,20 +305,16 @@ const renderPreviewBlock = (
291
305
  return renderTextBlock(`◇ ${block.label}`, block.text, width, theme, "muted");
292
306
  };
293
307
 
294
- /** Cached across picker openings, while the loader bounds process-wide Git work. */
295
- const statsLoader = createSessionStatsLoader();
296
- const statsCache = statsLoader.cache;
297
-
298
308
  const formatItemLabel = (
299
309
  session: SessionInfoLike,
300
310
  width: number,
301
311
  theme: any,
302
- statsCache: Map<string, SessionStats>,
312
+ statsState: SessionStatsState | undefined,
303
313
  ): string => {
304
314
  const itemWidth = width - 4;
305
315
  const timeStr = formatRelativeTime(session.modified);
306
316
 
307
- const stats = statsCache.get(session.path);
317
+ const stats = statsState?.status === "ready" ? statsState.stats : undefined;
308
318
  let statsStr = "";
309
319
  let statsLen = 0;
310
320
  if (stats && (stats.add > 0 || stats.mod > 0 || stats.del > 0)) {
@@ -319,6 +329,12 @@ const formatItemLabel = (
319
329
  (stats.del > 0 ? `${stats.del}`.length + 1 : 0) +
320
330
  parts.length -
321
331
  1;
332
+ } else if (statsState?.status === "pending") {
333
+ statsStr = theme.fg("dim", "…");
334
+ statsLen = 1;
335
+ } else if (statsState?.status === "unavailable") {
336
+ statsStr = theme.fg("warning", "?");
337
+ statsLen = 1;
322
338
  }
323
339
 
324
340
  const title = buildSessionLabel(session);
@@ -442,14 +458,28 @@ const renderPreview = (
442
458
 
443
459
  const loadSessionPreview = async (
444
460
  session: SessionInfoLike,
461
+ cache: ReturnType<typeof createSessionPreviewCache>,
462
+ signal: AbortSignal,
445
463
  ): Promise<SessionPreview> => {
446
- try {
447
- const manager = SessionManager.open(session.path);
448
- const context = manager.buildSessionContext();
449
- return buildSessionPreview(session, context.messages as any[]);
450
- } catch (error) {
451
- return buildPreviewError(session, error);
452
- }
464
+ const initialIdentity = await readPreviewFileIdentity(session.path);
465
+ const cached = cache.get(previewCacheKey(initialIdentity));
466
+ if (cached) return cached;
467
+
468
+ const data = await loadSessionPreviewData(session.path, { signal });
469
+ const preview = buildSessionPreview(
470
+ session,
471
+ data.messages as PreviewMessageLike[],
472
+ {
473
+ totalMessages: data.totalMessages,
474
+ truncatedBytes: data.truncatedBytes,
475
+ },
476
+ );
477
+ cache.set(
478
+ previewCacheKey(data.identity),
479
+ preview,
480
+ measureSessionPreviewBytes(preview),
481
+ );
482
+ return preview;
453
483
  };
454
484
 
455
485
  async function listSessions(
@@ -551,12 +581,17 @@ async function showSessionPicker(
551
581
  let previewScrollOffset = 0;
552
582
  let toolsExpanded = false;
553
583
  let thinkingVisible = false;
554
- const previewCache = new Map<string, SessionPreview>();
584
+ const previewCache = createSessionPreviewCache();
555
585
  let activePreview: SessionPreview | undefined;
556
586
  let previewTimer: ReturnType<typeof setTimeout> | undefined;
587
+ let previewController: AbortController | undefined;
557
588
  let previewSeq = 0;
558
- let statsController = new AbortController();
559
- let statsGeneration = 0;
589
+ // Stats use a picker-scoped, per-query first-observation cache. Rows that
590
+ // become visible later may observe later repository state; reopening the
591
+ // picker refreshes every row without polling repositories while idle.
592
+ const statsLoader = createSessionStatsLoader();
593
+ let statsUniverseVersion = 0;
594
+ let statsWindowSignature = "";
560
595
  let workspaceLoadSeq = 0;
561
596
  let disposed = false;
562
597
  let settled = false;
@@ -565,14 +600,22 @@ async function showSessionPicker(
565
600
  let showAllWorkspaces = false;
566
601
  let isLoading = false;
567
602
 
603
+ const cancelPreviewLoad = () => {
604
+ previewSeq++;
605
+ if (previewTimer) clearTimeout(previewTimer);
606
+ previewTimer = undefined;
607
+ previewController?.abort();
608
+ previewController = undefined;
609
+ };
610
+
568
611
  const disposePicker = () => {
569
612
  if (disposed) return;
570
613
  disposed = true;
571
614
  workspaceLoadSeq++;
572
- statsGeneration++;
573
- statsController.abort();
574
- previewSeq++;
575
- if (previewTimer) clearTimeout(previewTimer);
615
+ statsLoader.cancel();
616
+ cancelPreviewLoad();
617
+ previewCache.clear();
618
+ activePreview = undefined;
576
619
  };
577
620
  const finishPicker = (result: SessionInfoLike | null) => {
578
621
  if (settled) return;
@@ -580,30 +623,29 @@ async function showSessionPicker(
580
623
  disposePicker();
581
624
  done(result);
582
625
  };
583
- const previewKey = (session: SessionInfoLike): string =>
584
- `${session.path}:${session.modified.getTime()}`;
585
-
586
- const cancelStatsLoad = () => {
587
- statsGeneration++;
588
- statsController.abort();
589
- };
590
- const triggerStatsLoad = (list: SessionInfoLike[]) => {
591
- cancelStatsLoad();
592
- statsController = new AbortController();
593
- const generation = statsGeneration;
594
- const signal = statsController.signal;
595
- void statsLoader.load(list, signal, () => {
596
- if (disposed || signal.aborted || generation !== statsGeneration)
597
- return;
598
- tui.requestRender();
626
+ const ensureStatsWindowLoad = (visibleCount: number) => {
627
+ const targets = selectSessionStatsWindow(
628
+ filteredEntries.map((entry) => entry.session),
629
+ selectedPath,
630
+ visibleCount,
631
+ );
632
+ const signature = `${statsUniverseVersion}:${targets
633
+ .map((session) => session.path)
634
+ .join("\0")}`;
635
+ if (signature === statsWindowSignature) return;
636
+ statsWindowSignature = signature;
637
+ void statsLoader.reconcile(targets, sorted, () => {
638
+ if (!disposed) tui.requestRender();
599
639
  });
600
640
  };
601
641
 
602
- triggerStatsLoad(sorted);
603
-
604
642
  const loadWorkspaceSessions = async (allWorkspaces: boolean) => {
605
643
  const loadSeq = ++workspaceLoadSeq;
606
- cancelStatsLoad();
644
+ statsLoader.cancel();
645
+ statsWindowSignature = "";
646
+ cancelPreviewLoad();
647
+ previewCache.clear();
648
+ activePreview = undefined;
607
649
  isLoading = true;
608
650
  tui.requestRender();
609
651
  try {
@@ -616,11 +658,11 @@ async function showSessionPicker(
616
658
  if (disposed || loadSeq !== workspaceLoadSeq) return;
617
659
 
618
660
  sorted = sortSessions(results);
661
+ statsUniverseVersion++;
619
662
  for (const s of sorted) {
620
663
  sessionByPath.set(s.path, s);
621
664
  }
622
665
  entries = buildSessionSearchEntries(sorted);
623
- triggerStatsLoad(sorted);
624
666
 
625
667
  filteredEntries = filterSessionEntries(entries, filter);
626
668
  const matchedIndex = filteredEntries.findIndex(
@@ -648,6 +690,7 @@ async function showSessionPicker(
648
690
  };
649
691
 
650
692
  const schedulePreviewLoad = () => {
693
+ cancelPreviewLoad();
651
694
  previewScrollOffset = 0;
652
695
  const session = sessionByPath.get(selectedPath);
653
696
  if (!session) {
@@ -655,28 +698,45 @@ async function showSessionPicker(
655
698
  return;
656
699
  }
657
700
 
658
- const key = previewKey(session);
659
- const cached = previewCache.get(key);
660
- if (cached) {
661
- activePreview = cached;
662
- return;
663
- }
664
-
665
701
  activePreview = {
666
702
  title: buildSessionLabel(session),
667
703
  subtitle: `${session.modified.toLocaleString()} · loading preview`,
668
704
  blocks: [{ kind: "notice", text: "Loading selected session…" }],
669
705
  };
670
706
 
671
- if (previewTimer) clearTimeout(previewTimer);
672
- const seq = ++previewSeq;
707
+ const seq = previewSeq;
673
708
  previewTimer = setTimeout(() => {
674
- void loadSessionPreview(session).then((preview) => {
675
- if (seq !== previewSeq || selectedPath !== session.path) return;
676
- previewCache.set(key, preview);
677
- activePreview = preview;
678
- tui.requestRender();
679
- });
709
+ previewTimer = undefined;
710
+ const controller = new AbortController();
711
+ previewController = controller;
712
+ void loadSessionPreview(session, previewCache, controller.signal)
713
+ .then((preview) => {
714
+ if (
715
+ controller.signal.aborted ||
716
+ seq !== previewSeq ||
717
+ selectedPath !== session.path
718
+ ) {
719
+ return;
720
+ }
721
+ activePreview = preview;
722
+ tui.requestRender();
723
+ })
724
+ .catch((error) => {
725
+ if (
726
+ controller.signal.aborted ||
727
+ seq !== previewSeq ||
728
+ selectedPath !== session.path
729
+ ) {
730
+ return;
731
+ }
732
+ activePreview = buildPreviewError(session, error);
733
+ tui.requestRender();
734
+ })
735
+ .finally(() => {
736
+ if (previewController === controller) {
737
+ previewController = undefined;
738
+ }
739
+ });
680
740
  }, PREVIEW_LOAD_DEBOUNCE_MS);
681
741
  };
682
742
 
@@ -692,7 +752,12 @@ async function showSessionPicker(
692
752
  ): SelectItem[] =>
693
753
  current.map((entry) => ({
694
754
  value: entry.session.path,
695
- label: formatItemLabel(entry.session, listWidth, theme, statsCache),
755
+ label: formatItemLabel(
756
+ entry.session,
757
+ listWidth,
758
+ theme,
759
+ statsLoader.get(entry.session),
760
+ ),
696
761
  }));
697
762
 
698
763
  const rebuild = () => {
@@ -728,20 +793,21 @@ async function showSessionPicker(
728
793
  );
729
794
  container.addChild(new Spacer(1));
730
795
  } else {
731
- const items = buildItems(filteredEntries, width);
732
- selectList = new SelectList(
733
- items,
734
- Math.max(1, Math.min(maxVisible, Math.max(items.length, 1))),
735
- {
736
- selectedPrefix: (text) =>
737
- theme.fg(focus === "list" ? "accent" : "muted", text),
738
- selectedText: (text) =>
739
- theme.fg(focus === "list" ? "accent" : "muted", text),
740
- description: (text) => theme.fg("muted", text),
741
- scrollInfo: (text) => theme.fg("dim", text),
742
- noMatch: () => theme.fg("warning", " No matching sessions"),
743
- },
796
+ const effectiveVisible = Math.max(
797
+ 1,
798
+ Math.min(maxVisible, Math.max(filteredEntries.length, 1)),
744
799
  );
800
+ ensureStatsWindowLoad(effectiveVisible);
801
+ const items = buildItems(filteredEntries, width);
802
+ selectList = new SelectList(items, effectiveVisible, {
803
+ selectedPrefix: (text) =>
804
+ theme.fg(focus === "list" ? "accent" : "muted", text),
805
+ selectedText: (text) =>
806
+ theme.fg(focus === "list" ? "accent" : "muted", text),
807
+ description: (text) => theme.fg("muted", text),
808
+ scrollInfo: (text) => theme.fg("dim", text),
809
+ noMatch: () => theme.fg("warning", " No matching sessions"),
810
+ });
745
811
  const selectedIndex = filteredEntries.findIndex(
746
812
  (entry) => entry.session.path === selectedPath,
747
813
  );
@@ -811,23 +877,21 @@ async function showSessionPicker(
811
877
  leftLines.push("");
812
878
  leftLines.push(theme.fg("muted", " Loading sessions..."));
813
879
  } else {
814
- const items = buildItems(filteredEntries, layout.listWidth);
815
- selectList = new SelectList(
816
- items,
817
- Math.max(
818
- listHeight,
819
- Math.min(maxVisible, Math.max(items.length, 1)),
820
- ),
821
- {
822
- selectedPrefix: (text) =>
823
- theme.fg(focus === "list" ? "accent" : "muted", text),
824
- selectedText: (text) =>
825
- theme.fg(focus === "list" ? "accent" : "muted", text),
826
- description: (text) => theme.fg("muted", text),
827
- scrollInfo: (text) => theme.fg("dim", text),
828
- noMatch: () => theme.fg("warning", " No matching sessions"),
829
- },
880
+ const effectiveVisible = Math.max(
881
+ listHeight,
882
+ Math.min(maxVisible, Math.max(filteredEntries.length, 1)),
830
883
  );
884
+ ensureStatsWindowLoad(effectiveVisible);
885
+ const items = buildItems(filteredEntries, layout.listWidth);
886
+ selectList = new SelectList(items, effectiveVisible, {
887
+ selectedPrefix: (text) =>
888
+ theme.fg(focus === "list" ? "accent" : "muted", text),
889
+ selectedText: (text) =>
890
+ theme.fg(focus === "list" ? "accent" : "muted", text),
891
+ description: (text) => theme.fg("muted", text),
892
+ scrollInfo: (text) => theme.fg("dim", text),
893
+ noMatch: () => theme.fg("warning", " No matching sessions"),
894
+ });
831
895
  const selectedIndex = filteredEntries.findIndex(
832
896
  (entry) => entry.session.path === selectedPath,
833
897
  );
@@ -909,8 +973,11 @@ async function showSessionPicker(
909
973
  return renderSplitPane(width);
910
974
  },
911
975
  invalidate: () => {
976
+ cancelPreviewLoad();
912
977
  previewCache.clear();
978
+ activePreview = undefined;
913
979
  rebuild();
980
+ schedulePreviewLoad();
914
981
  },
915
982
  dispose: disposePicker,
916
983
  handleInput: (data) => {
@@ -0,0 +1,104 @@
1
+ import type { PreviewFileIdentity } from "./preview-loader.ts";
2
+ import type { SessionPreview } from "./sessions.ts";
3
+
4
+ export const PREVIEW_CACHE_MAX_ENTRIES = 16;
5
+ export const PREVIEW_CACHE_MAX_BYTES = 4 * 1024 * 1024;
6
+
7
+ interface PreviewCacheOptions {
8
+ maxEntries?: number;
9
+ maxBytes?: number;
10
+ }
11
+
12
+ interface CacheEntry {
13
+ preview: SessionPreview;
14
+ bytes: number;
15
+ }
16
+
17
+ export function measureSessionPreviewBytes(value: unknown): number {
18
+ if (typeof value === "string") return Buffer.byteLength(value, "utf8");
19
+ if (Array.isArray(value)) {
20
+ return value.reduce(
21
+ (total, nested) => total + measureSessionPreviewBytes(nested),
22
+ 0,
23
+ );
24
+ }
25
+ if (typeof value !== "object" || value === null) return 0;
26
+ return Object.entries(value).reduce(
27
+ (total, [key, nested]) =>
28
+ total +
29
+ Buffer.byteLength(key, "utf8") +
30
+ measureSessionPreviewBytes(nested),
31
+ 0,
32
+ );
33
+ }
34
+
35
+ export function previewCacheKey(identity: PreviewFileIdentity) {
36
+ return [
37
+ identity.path,
38
+ identity.device,
39
+ identity.inode,
40
+ identity.size,
41
+ identity.mtimeNs,
42
+ ].join(":");
43
+ }
44
+
45
+ export function createSessionPreviewCache(options: PreviewCacheOptions = {}) {
46
+ const maxEntries = Math.max(
47
+ 1,
48
+ options.maxEntries ?? PREVIEW_CACHE_MAX_ENTRIES,
49
+ );
50
+ const maxBytes = Math.max(1, options.maxBytes ?? PREVIEW_CACHE_MAX_BYTES);
51
+ const cache = new Map<string, CacheEntry>();
52
+ let totalBytes = 0;
53
+ let evictionCount = 0;
54
+
55
+ const remove = (key: string) => {
56
+ const entry = cache.get(key);
57
+ if (!entry) return false;
58
+ cache.delete(key);
59
+ totalBytes -= entry.bytes;
60
+ return true;
61
+ };
62
+
63
+ const get = (key: string) => {
64
+ const entry = cache.get(key);
65
+ if (!entry) return undefined;
66
+ cache.delete(key);
67
+ cache.set(key, entry);
68
+ return entry.preview;
69
+ };
70
+
71
+ const set = (key: string, preview: SessionPreview, bytes: number) => {
72
+ remove(key);
73
+ if (!Number.isFinite(bytes) || bytes < 0 || bytes > maxBytes) return false;
74
+
75
+ cache.set(key, { preview, bytes });
76
+ totalBytes += bytes;
77
+ while (cache.size > maxEntries || totalBytes > maxBytes) {
78
+ const oldest = cache.keys().next().value;
79
+ if (oldest === undefined || !remove(oldest)) break;
80
+ evictionCount++;
81
+ }
82
+ return cache.has(key);
83
+ };
84
+
85
+ const clear = () => {
86
+ cache.clear();
87
+ totalBytes = 0;
88
+ };
89
+
90
+ return {
91
+ get,
92
+ set,
93
+ clear,
94
+ get entries() {
95
+ return cache.size;
96
+ },
97
+ get bytes() {
98
+ return totalBytes;
99
+ },
100
+ get evictions() {
101
+ return evictionCount;
102
+ },
103
+ };
104
+ }