mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Refs #5718. ## What happens `appendAgentEvents` evicts the per-agent live observer journal back to *exactly* `MAX_OBSERVER_EVENTS`: ```ts const trimmed = sorted.length > MAX_OBSERVER_EVENTS; const final = trimmed ? sorted.slice(sorted.length - MAX_OBSERVER_EVENTS) : sorted; ``` Once an agent's journal reaches 3000, `current.length` is 3000 forever, every later append makes `sorted.length >= 3001`, and `trimmed` is `true` on every call. That permanently disables the incremental-fold gate: ```ts if (allAtEnd && !trimmed) { /* incremental fold */ } else { transcriptByAgent.set(key, buildTranscriptState(final)); } ``` So every steady-state append then replays the whole retained window through `buildTranscriptState`, which is itself O(streamed-text) because streaming chunks fold as uncapped string concat. Nothing shrinks `eventsByAgent` except a store reset, so the state is permanent for the life of the renderer process, per agent. At ~90 frames/min an agent crosses the cap in ~33 minutes; from then on live CPU escalates (issue receipts: 188x on a headless ingest, renderer CPU climbing to 119% of a core after five minutes idle). This is not an off-by-one — a cap of 3000 does want `>`. The defect is that trimming *to* the cap re-arms eviction on the very next append, and eviction is what forces the replay. ## Fix Evict to a low-water mark below the cap: ```ts const OBSERVER_EVENTS_LOW_WATER = Math.floor(MAX_OBSERVER_EVENTS * 0.9); ``` The journal still never exceeds `MAX_OBSERVER_EVENTS`; it now has to be refilled by ~300 ordinary appends before the next eviction, so one replay is amortized across the appends that refill it. Retention semantics (newest-N at trim time) and the derived transcript are unchanged. The mark is a **fraction of the cap** rather than a fixed count so the math stays correct if the cap is ever made per-agent — a fixed headroom could exceed a smaller cap and drive the slice length negative. ### Eviction floor Low-water eviction leaves headroom below the cap, and the dedup set is built only from the *retained* array — so once eviction discards the oldest frames, the journal no longer remembers them. A relay reconnect replaying a pre-eviction frame (normal relay behavior, and the reason the dedup set exists) would be re-admitted into the headroom, and a later refill to the cap would then trim away up to 300 legitimate retained events with **no new activity** — a bounded display-window loss plus rebuild churn that partially defeats the amortization. To close that, each agent carries an **eviction floor**: the ordering key of the newest event eviction has ever discarded (`evictionFloorByAgent`, recorded at trim time as the entry just below the retained window). `appendAgentEvents` rejects any arrival at or before the floor (`isObserverEventAfter`, so an equal key is rejected — the floor event itself was evicted); a stale-only batch returns `false` with no rebuild and no notify. Out-of-order frames *newer* than the floor are still admitted via the rebuild fallback, so the fold-gate semantics are unchanged. The floor is cleared in `resetAgentObserverStore` alongside the other per-agent maps. ## Evidence `observerTranscriptRetention.test.mjs` asserts the retention window's **shape** — the observable signal for which ingest path runs, since transcript *content* is identical on both paths by design — plus boundary cases and the invariant that the derived transcript still equals a full replay of the retained window. Against the pre-fix trim-to-cap shape, three tests fail on the mechanism itself (`test_append_crossing_cap_trims_to_exactly_low_water`, `test_headroom_refills_before_next_eviction`, `test_single_batch_larger_than_cap_trims_to_low_water` — each expects headroom the old shape never leaves), and the cost shows up directly in runtime: | | `observerTranscriptRetention.test.mjs` (single-event appends past the cap) | |---|---| | trim-to-cap (pre-fix) | **429,105 ms** | | this branch | **16,221 ms** | ~26x on this workload, consistent with the 188x the issue measured on a heavier one (their events accumulate streaming text; these do not, so this understates it). Three further tests pin the **eviction floor** against reconnect replay: a replay of already-evicted frames leaves the retained window byte-identical and notifies no listener; a pre-floor frame arriving after a refill to the cap drops no retained events; and an out-of-order frame *newer* than the floor is still admitted. Deleting the floor check turns exactly the first two red while the out-of-order case stays green — confirming the tests pin the floor's rejection without over-constraining legitimate out-of-order delivery. ## Merge-order note This PR collides with #5596 (bounded renderer accumulators) on `observerRelayStore.ts` by design — #5596 refactors this exact eviction into `mergeObserverEventBatch` in a new `observerEventOrdering.ts` and adds a second, unpinned-agent tier (`truncateUnpinnedAgentWindow`, `UNPINNED_AGENT_EVENT_TAIL`). This PR merges first; #5596 rebases over it, porting the low-water cap-math **and the per-agent eviction floor** into `mergeObserverEventBatch`, and applying the same headroom to the unpinned-tier truncate (which must also record a floor when it trims). The fraction-of-cap form makes the low-water port mechanical — it feeds either the 3000 pinned cap or the 100 unpinned tail without a fixed-count underflow. ## Credits Supersedes #5767 (Chessing234's low-water-mark approach and the runtime measurements). Closes #5718. Issue receipts from the reporter, GeneralJah215 (188x headless, 119%/core after 5min idle). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Buzz
Desktop chat shell with:
- Tauri + React + TypeScript + Vite
- Tailwind CSS
- shadcn/ui-ready shared components
- Biome (lint/format/check)
- Feature-driven frontend structure
Scripts
pnpm dev- run the web frontendpnpm tauri dev- run the desktop apppnpm build- typecheck and build frontendpnpm typecheck- TypeScript checkspnpm lint- Biome lintpnpm format- Biome format (write)pnpm check- Biome check
Structure
src/shared- reusable app-wide code (ui,lib,styles)src/features- feature modules (vertical slices)src/app- top-level app composition