Repository navigation
fix(ghosts): keep superseded ghosts of the live turn until it ends - #1228
Merged
Merged
Conversation
gcSupersededGhosts evicted a superseded ghost 5 s after supersedure even while its turn was still semantic.currentTurn. ghostsFromSemanticTurn re-mints any missing block of the current turn on every tick, so the same uuid came back with no supersededBy; its committed entry was already reconciled, so it orphaned 30 s later newer than the JSONL tail and painted as a duplicate of the committed block. The ghost logs on disk held 3,231 of these re-mints in 419 of 1,955 logs (2,128 Codex, 1,103 Claude; 93% tool_use), each at least 5,040 ms after supersedure: the 5 s grace plus the 1 s sweep. The exemption is the one gcHiddenOrphanGhosts already has. Fixes #730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1228: the fix was one argument at the call site, and passing null there type-checked and survived every test. The sweep's three reducers now compose in ghosts.ts, which derives currentTurnId from the runtime slice; the timer calls it. Tests drive the composed tick and pin the supersede guard in ghostsFromSemanticTurn (both mutants now killed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review of #1228: swapping the two timings inside sweepGhosts passed every test. A ghost 10 s quiet must stay live and orphan after 30 s; the swap now fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
Sep 25, 2026
Keeps #1228's sweepGhosts and drops ghostsToPersist, which this branch removes along with the on-disk log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05
added a commit
that referenced
this pull request
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.
Fixes #730
What was wrong
gcSupersededGhostsevicted a superseded ghost 5 s after supersedure, even while its turn was stillsemantic.currentTurn(for example, a tool still running).ghostsFromSemanticTurnre-mints any missing block of the current turn on every tick, so the same uuid came back withoutsupersededBy. Its committed entry had already been reconciled, so nothing superseded it again. 30 s later it orphaned with anupdatedAtnewer than the JSONL tail, which is the state the render predicate still paints: a duplicate of the committed block.Evidence (counted, not reasoned)
I scanned all 1,955 ghost logs in
~/Library/Application Support/agent-code/ghost-logsfor a uuid written superseded and then written again un-superseded:resp_…) and 1,103 Claude (msg_…).tool_use, 137text, 84thinking.The fixture
testing/fixtures/ghost-remint/is one real Claude Bashtool_useghost, unedited: superseded at +2.9 s, re-minted with a freshcreatedAtat +8.9 s, orphaned at +38.9 s.Fix
gcSupersededGhoststakescurrentTurnIdand spares that turn's ghosts. This is the same exemptiongcHiddenOrphanGhostsgot in #725, and the sweep inuseIpcSubscriptions.tspasses the same id to both. Retention is bounded to one turn's blocks.Tests
ghosts.remint.test.tsdrives the real functions with the fixture: mint, reconcile against the committed entry, sweep at the recorded re-mint time, then the next semantic tick.supersededBy.recentCommandHistoryunder Node 25 (localStorage.clear); it passes on Node 24, so it is environmental and unrelated.tsc -bis clean.Not verified
useIpcSubscriptions.tshas no test; the replay harness omits the timer by design (seereconstructSlices.ts).🤖 Generated with Claude Code
Review disposition (A Codex, B Pi; 2 rounds)
3d95d941):sweepGhostscomposes the three reducers and readscurrentTurnitself; the composed tick is testedghostsFromSemanticTurnwas unpinnedsweepGhostssurvived89c0e51d): orphan-TTL test through the composed tick>=versus>at the eviction boundaryRound 2: A and B both MERGE-READY. Recount of the on-disk evidence by B: 419 logs (exact), minimum gap 5,040 ms (exact).