Files
buzz/desktop
17977814d3 fix(desktop): amortize observer journal eviction with a low-water mark (#5808)
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>
2026-08-14 11:38:18 -04:00
..
2026-08-03 21:51:17 -04:00
2026-08-14 16:29:50 +01:00

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 frontend
  • pnpm tauri dev - run the desktop app
  • pnpm build - typecheck and build frontend
  • pnpm typecheck - TypeScript checks
  • pnpm lint - Biome lint
  • pnpm 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