import type { AgentBrowserCdpEvent, AgentBrowserCdpEventPage, } from '../../shared/agent-browser'; type GapReason = NonNullable['reason']; interface BufferedEvent { event: AgentBrowserCdpEvent; byteLength: number; } interface GapMarker { sequence: number; reason: GapReason; } export interface AgentBrowserEventBufferOptions { maxEvents?: number; maxBytes?: number; } const DEFAULT_MAX_EVENTS = 10_000; const DEFAULT_MAX_BYTES = 16 * 1024 * 1024; export class AgentBrowserEventBuffer { private readonly events: BufferedEvent[] = []; private readonly gaps: GapMarker[] = []; private readonly maxEvents: number; private readonly maxBytes: number; private bytes = 0; private sequence = 0; private evictedThrough = 0; constructor(options: AgentBrowserEventBufferOptions = {}) { this.maxEvents = options.maxEvents ?? DEFAULT_MAX_EVENTS; this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; } get cursor(): number { return this.sequence; } push(event: Omit): AgentBrowserCdpEvent { const complete = { ...event, sequence: ++this.sequence }; const byteLength = Buffer.byteLength(JSON.stringify(complete)); this.events.push({ event: complete, byteLength }); this.bytes += byteLength; this.evict(); return complete; } markGap(reason: GapReason): number { const sequence = ++this.sequence; this.gaps.push({ sequence, reason }); this.pruneGaps(); return sequence; } read(after = 0, methods?: string[], limit = 100): AgentBrowserCdpEventPage { const safeAfter = Math.max(0, Math.trunc(after)); const safeLimit = Math.min(Math.max(Math.trunc(limit), 1), 200); const methodSet = methods?.length ? new Set(methods) : null; const start = Math.max(safeAfter, this.evictedThrough); const selected: AgentBrowserCdpEvent[] = []; let scannedThrough = start; for (const buffered of this.events) { if (buffered.event.sequence <= start) continue; scannedThrough = buffered.event.sequence; if (!methodSet || methodSet.has(buffered.event.method)) { selected.push(buffered.event); if (selected.length >= safeLimit) break; } } if (selected.length < safeLimit) { scannedThrough = Math.max(scannedThrough, this.sequence); } const nextCursor = Math.max(safeAfter, scannedThrough); const hasMore = this.events.some(({ event }) => event.sequence > nextCursor && (!methodSet || methodSet.has(event.method))); const oldestAvailable = this.events[0]?.event.sequence ?? this.sequence + 1; const gap = safeAfter < this.evictedThrough ? { reason: 'evicted' as const, oldestAvailable } : this.gaps.find((marker) => marker.sequence > safeAfter && marker.sequence <= nextCursor); return { events: selected, nextCursor, hasMore, ...(gap ? { gap: { reason: gap.reason, oldestAvailable, }, } : {}), }; } clear(): void { this.events.length = 0; this.gaps.length = 0; this.bytes = 0; this.sequence = 0; this.evictedThrough = 0; } private evict(): void { while (this.events.length > this.maxEvents || this.bytes > this.maxBytes) { const removed = this.events.shift(); if (!removed) break; this.bytes -= removed.byteLength; this.evictedThrough = Math.max(this.evictedThrough, removed.event.sequence); } this.pruneGaps(); } private pruneGaps(): void { while (this.gaps[0] && this.gaps[0].sequence <= this.evictedThrough) { this.gaps.shift(); } } }