Repository navigation
fix(storage): prune orphaned ghost logs; protect a ghost log only while its session exists - #1223
Merged
Merged
Conversation
…le its session exists Every ghost log was protected from every retention pass as a bucket, so the ghost-logs cap could never act, and startup compaction rewrites each file, so none ever aged either. Measured on the owner's machine: 1,952 ghost logs, 2.1 GB, of which 1,935 (2,128 MB) belonged to sessions no longer in any window's workspace; only 17 (49 MB) were real recovery state. A ghost log is recovery state while its session exists: in any window's committed workspace, or running in the session manager. Main now provides that owner set to retention (read once per prune). Owned logs stay fully protected however old; orphaned logs go through TTL, cap and budget like any other debug output, still behind the 10-minute active grace (so a pane closed a moment ago can be restored by undo). With no owner set yet (the first prune runs before the workspace store opens) or a failing provider, everything stays protected as before. Tests: orphans trimmed oldest-first under the cap while an older owned log stays; an orphan ages out and an owned log does not; a fresh orphan is kept inside the grace. Unprotecting all ghost logs turns two red. Fixes #732 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… never none Steering review (urgent, before review): WorkspaceFileStore loads an EMPTY file and goes read-only when workspace.json is unreadable, corrupt or from a newer version, precisely so the user's real data is not overwritten. Its sessionIds() is then empty although the file on disk may own every session, and the provider handed retention that empty set as 'no owners', which would delete every ghost log in recovery mode. ghostLogOwnersFrom() now returns null (unknown, so protect all) for a read-only store, and main uses it. Tests: a read-only store yields null and an old owned log survives even an over-cap prune; a healthy store's set is its sessions plus the running ones. Removing the read-only guard turns the first red. The 1,935-orphan measurement matched every session-id-shaped UUID anywhere in workspace.json (all committed windows), not one window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 25, 2026
…e.json was read Review round 1 on #1223 (2 Claude + 1 Pi): - Major (B): a MISSING or empty workspace.json is a fresh install to the store, not read-only, so sessionIds() was an empty set that retention trusted, deleting every ghost log of a workspace the user had only moved aside (this machine's state dir shows the habit). The store now records whether this run read a COMPLETE file (parser completeness, as tmuxRecovery already does), and ghostLogOwnersFrom() returns unknown otherwise: missing, empty, partial or unreadable. A later save does not upgrade it. Store tests use the real sanitized v3 workspace for the trusted case and a damaged copy for the partial one. - Major (A, test gap): the provider -> policy -> prune wiring and the throw-means-unknown guard were removable with everything green. New real-file test through pruneDebugStorage: an old orphan is deleted and an old owned log kept; unknown owners and a throwing provider delete nothing. Each mutation (wiring dropped, throw read as 'no owners', completeness ignored, trust by default) now turns a test red. - Comments (A, B): the recovery-contract wording claimed ghost logs are what a restored pane rebuilds from; the review found nothing has read one on restore since 0cb71e9 (filed as #1225), and that undo respawns under a fresh id. Comments now say what the code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
This was referenced Sep 25, 2026
Closed
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 (#732, class C6)
isProtectedFromDebugPruneprotected the wholeghost-logsbucket from every pass. The ghost-logs cap (8 % of the budget) could never act. Startup compaction rewrites every file, so none ever aged out either.Measured on the owner's machine (2026-09-25): 1,952 ghost logs, 2.1 GB. Of those, 1,935 (2,128 MB) belong to sessions no longer in any window's workspace; only 17 (49 MB) are real recovery state.
Change
WorkspaceFileStore.sessionIds()) or running in the session manager (a brand-new pane can run before its first save).index.tssets that owner provider right after the workspace store opens, and retention reads it once per prune.Tests (fail-first)
Unprotecting all ghost logs (mutation) turns two tests red; the original code fails the first two.
Verification
npx tsc -bclean;src/main/storagepasses (8 files, 101 tests).Not verified here: a real prune over the 2.1 GB directory in the running app. The first scheduled prune after this lands should free about 1.9 GB (the cap is 8 % of the debug budget).
Out of scope
#731 (resume re-appends the whole ghost log) makes owned logs grow per resume. That is its own fix.
Fixes #732
🤖 Generated with Claude Code
Review disposition (round 1 → round 2, reviewers A/B/C)
78934bf2: owners are unknown unlessparseWorkspaceFilereports complete (sessionOwnershipKnown())debugRetention.ghostOwners.test.tsruns throughpruneDebugStorageon real filesisProtectedFromDebugPrunegateRound 2: A, B and C all MERGE-READY on
78934bf2.