Repository navigation
Description
Symptom
The renderer's live entry window is supposed to hold at most MAX_LIVE_ENTRIES (2,000) / MAX_LIVE_ENTRY_BYTES (32 MiB) entries per session (#375). Over the 2026-09-01 → 09-03 run the renderer.session.memory.* gauges (~/.config/agent-code/performance/runs/2026-09-01T20-33-12-main-75074/metrics.jsonl, 133,564 samples per gauge) show:
| gauge | max |
|---|---|
entries (length) |
5,016 — 1,924 samples above the 2,000 cap |
ghostMap (size) |
917 in one session |
seenUuids |
5,077 |
usedJSHeapSize |
1.13 GB |
So the count cap was not being enforced for long stretches, and one session accumulated ~900 ghosts.
Root cause
Two interacting rules:
computeProtectBound(src/renderer/src/session-runtime/liveEntryWindow.ts~L345) lowers the trim bound to theupdatedAtof every un-superseded ghost, including orphaned ones.planLiveEntryTrimthen stops at the first entry at-or-newer than that bound.- The 1 s sweep (
useIpcSubscriptions.ts~L495) callsorphanStale(flags, never removes) andgcSupersededGhosts(removes only superseded ghosts). An orphaned ghost that JSONL never matches — every sidecar-shaped stream (title generation, next-prompt prediction) and any ghost of a turn whose committed entry arrives under a different id — stays inruntime.ghostsfor the life of the session.
Once a single orphaned ghost exists, the protect bound is pinned at its updatedAt forever: every entry ingested after that instant is "protected", the planner returns null, and runtime.entries becomes append-only. The ghost map itself also grows monotonically (917 above).
The render predicate (rendering/model/ghostPredicate.ts rule 4, mergedEntries.ts) already treats an orphaned ghost whose updatedAt <= lastJsonlEntryAt as permanently hidden ("stale orphan from earlier in the session, JSONL kept writing past it"). Such a ghost can never paint again, so it neither needs its committed owner protected nor needs to stay in memory.
Expected
- A ghost pins the trim bound only while it can still render. Orphaned ghosts already hidden by the JSONL-tail rule must not pin it.
- Those same ghosts are evicted from
runtime.ghostsafter a short grace (they are already persisted to the ghost log at the orphan transition), keeping the map bounded by "orphans newer than the JSONL tail" — which is exactly the set the fallback render needs. - No change to the render predicate, the orphan TTL, or which ghosts render (see
docs/design/ghost-system.mdWarning section).
Impact
Per-session renderer heap grows without bound with transcript length (28 MB of entries observed in one session, 5k entries), plus selectMergedEntries walks a 900-ghost map on every TileLeaf render. Contributes to the 1.1 GB renderer heap and the multi-second long tasks seen on 2026-09-03.
Environment
Agent Code main @ 6012427, Electron 43, macOS 15, 8-core M3 / 16 GB.
Activity
perf(renderer): keep the live turn's hidden orphans and document evic…