Repository navigation
perf(renderer): stop hidden orphan ghosts from pinning the live-entry trim - #725
Merged
Merged
Conversation
The two-day journal shows the live entry window at 5,016 entries against its 2,000 cap and one session holding 917 ghosts. Record the design before touching the ghost subsystem, whose design doc asks for exactly that. Refs #724 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
… trim computeProtectBound lowered the trim bound to every un-superseded ghost's updatedAt, orphaned or not, and the sweep only ever removed superseded ghosts. One orphan that JSONL never matched therefore pinned the bound for the rest of the session: planLiveEntryTrim returned null on every burst and runtime.entries became append-only (5,016 entries against the 2,000 cap on the two-day journal, one session holding 917 ghosts). The render predicate's rule 4 already defines a ghost that can never paint again — orphaned and at-or-before lastJsonlEntryAt, which only advances. The trim bound now skips exactly those ghosts, and the sweep evicts them after the same grace superseded ghosts get. Orphans newer than the tail, the stuck-JSONL fallback the ghost system exists for, still pin and still stay. Rules 1–5, the orphan TTL, and which ghosts render are unchanged. Fixes #724 Refs #375, #103 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…tion Review follow-ups: ghostsFromSemanticTurn re-mints any block of semantic.currentTurn whose uuid is missing, so evicting a hidden orphan of a turn that is still current would re-create it un-orphaned with a fresh updatedAt — churn on every tick and a reset of rule 4's clock. The sweep now passes the current turn id and gcHiddenOrphanGhosts leaves those ghosts alone until the turn is no longer current. Also: the rule-4 predicate moves into ghosts.ts (the entry window imports it, not the other way round), the grace boundary matches gcSupersededGhosts, the sweep's debug record names evictions, the design doc gains an eviction section and lists all three orphanedAt consumers, and an end-to-end renderer test drives mint → orphan → history roll-off → trim → eviction through useIpcSubscriptions. Refs #724 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
Owner
Author
|
Self-review pass (adversarial review of the first cut) — resolved in HEAD:
Noted for follow-up issues rather than this PR: the same re-mint hazard exists in |
This was referenced Sep 3, 2026
Closed
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The renderer's live entry window (#375) is capped at 2,000 entries / 32 MiB per session, but the two-day performance journal shows
renderer.session.memory.entriesat 5,016 (1,924 samples over the cap) and one session'sghostMapat 917. Full evidence in #724.Cause:
computeProtectBoundlowered the trim bound to theupdatedAtof every un-superseded ghost, orphaned or not, while the 1 s sweep only ever removed superseded ghosts. One orphan that JSONL never matches (every sidecar-shaped stream, for instance) pinned the bound for the rest of the session —planLiveEntryTrimreturnednullon every burst andruntime.entriesbecame append-only. The ghost map itself also grew monotonically.What changed
The render predicate's rule 4 (
ghostPredicate.ts, mirrored inmergedEntries.ts) already defines a ghost that can never paint again: orphaned and at-or-beforelastJsonlEntryAt, which only moves forward. This PR makes the two other consumers of the ghost map agree with that definition instead of changing it:liveEntryWindow.ts: newisGhostHiddenBehindJsonlTail;computeProtectBoundskips those ghosts;planLiveEntryTrimtakeslastJsonlEntryAtas an optional fourth argument (null keeps the previous behaviour, so existing callers and tests are unchanged).ghosts.ts: newgcHiddenOrphanGhostsreducer, reference-stable on no-op, that evicts exactly that set once the ghost has been orphaned forGHOST_SUPERSEDED_GC_MS(the orphan transition already persisted it to the ghost log).useIpcSubscriptions.ts: the sweep chains the new reducer aftergcSupersededGhosts; the JSONL-burst trim passes the post-burst tail; theghost_orphan_sweepfeed-debug record gainsevictedCount.Not changed, deliberately (see the Warning section of
docs/design/ghost-system.md): rules 1–5, the orphan TTL,orphanStale,gcSupersededGhosts, and therefore which ghosts render. Orphans newer than the JSONL tail — the stuck-JSONL fallback — still pin the bound and still stay in the map.Linked issues
Fixes #724. Refs #375 (the window this restores), #103, #365.
Verification
NODE_ENV=test npx vitest run --project unit src/renderer/src/session-runtime/ src/renderer/src/workspace/— 33 files, 391 tests passed. New contracts inentries.test.ts(hidden orphan no longer bounds the trim; an orphan newer than the tail still does; a null tail keeps today's behaviour) and a newghosts.test.tsforgcHiddenOrphanGhosts(evicts only orphaned, un-superseded, at-or-before-tail ghosts after the grace; keeps orphans ahead of the tail, un-orphaned ghosts, everything with a null tail; leaves superseded ghosts to the existing reducer; reference-stable).--project rendererfor the same directories — passed.npx tsc -b --pretty false— clean.renderer.session.memory.entriesshould plateau at the cap andghostMapshould stop growing on the next long run.Known limitations
updatedAtagainst a producer JSONL timestamp.Plan:
docs/superpowers/plans/2026-09-03-orphan-ghosts-unpin-trim.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa