Skip to content

bug(ghosts): after a reload, a semantic replay re-mints ghosts of already-committed blocks #1233

Open

Description

@Juliusolsson05

Found in the #1228 review (reviewer A). This is a different route to the #730 duplicate, and it predates that PR.

  1. A ghost is superseded and persisted.
  2. The app reloads. readGhostLog drops superseded ghosts (src/main/ghostJournal.ts:265-271), so the renderer has no marker for that uuid.
  3. The committed history loads.
  4. A semantic replay opens the same turn and block. Codex rollout replay leaves semantic.currentTurn open (useIpcSubscriptions.ts near 2718).
  5. ghostsFromSemanticTurn sees an empty slot and mints a fresh, unsuperseded ghost. The semantic handler never reconciles it against the runtime.entries already loaded.
  6. 30 s after the turn clears, it orphans with an updatedAt newer than the old tail, and selectMergedEntries paints a duplicate.

Evidence: a temporary reproduction with the #1228 fixture's first 4 records gave a reduceGhostLogSansSuperseded size of 0, and then painted both the committed row and the ghost.

Evidence still needed: a recorded reload in which the replay actually re-opens a committed turn (Codex rollout replay is the suspect).

Fix direction: reconcile newly minted ghosts against the committed entries already held, for example by message.id, codexTurnId or tool-use id through the existing indexes. Alternatively, keep superseded markers in the log reduction for the current tail.

Refs #730, #1228, #1231.

Activity

  1. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Re-audit after #1235 (quality loop, 2026-09-25): steps 1–2 of the sequence no longer exist. #1235 removed the on-disk ghost log (src/main/ghostJournal.ts, readGhostLog are gone from main), so there are no persisted superseded ghosts to drop across a reload.

    What remains is narrower. After a reload, if a semantic replay re-opens a turn whose entries are ALREADY loaded, ghostsFromSemanticTurn mints a ghost that nothing supersedes (reconcileUpstream reconciles newly arriving entries). After the orphan TTL it could paint a duplicate.

    That still needs a recorded reload in which a replay actually re-opens a committed turn; Codex rollout replay is the suspect. Keeping this needs-evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    class:C8-renderingRendering order/duplicatesneeds-evidenceNeeds a recording/real use to settlesev:P2Real bug with a workaroundtype:bugSomething works wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions