[Bug fix + FR] Read-side recovery for sessions with dangling logicalParentUuid (lost+found / fsck)
Summary
When the chain walker hits a compact_boundary whose logicalParentUuid doesn't resolve (the write-side bug at #46603 / #49996 / compact.ts:598), the session silently truncates at the boundary with no UI feedback. The walker just stops; the user sees a partial chain and has no way to know that pre-compaction history is intact on disk but unreachable. Even after the write-side fix lands, every JSONL already on disk with a phantom lpu stays unrecoverable.
This issue requests a defensive read-side mitigation: at session-load time, detect dangling boundary stitches and reattach the orphaned content via the earlier message in the same file, surfacing a visible alert in the rendered transcript. fsck's lost+found for sessions.
Why a read-side fix matters even after the write-side fix
- The dangling-lpu pattern is intermittent (depends on what
messages.at(-1)happened to be at compact-time; see #46603 comment). Heavy users have it in 5–20% of their auto-compacted sessions in my sample. - Once written, the JSONL's lpu can never be repaired; the predecessor message simply doesn't exist anywhere on disk.
- The current behavior (silent truncation) is the worst possible UX: scrollback /
--resumeshows ~⅔ of the conversation, the user thinks they lost work, no error, no signal. Several existing reports (#46603 OP, #48937, #49996) describe this symptom from the user's seat. - Future write-side regressions that bypass the L598 fix would also be invisible without read-side detection.
Proposed behavior
At load time, after the existing parentUuid walk and the (proposed in #48937) logicalParentUuid walk including cross-file lookup both fail to reach the chain root:
- Detect any
compact_boundarywhoselogicalParentUuidreferences a uuid not present in the in-memory message map after sibling-file resolution. - Reattach by setting the boundary's lpu to the in-file message immediately preceding it (typically the actual last pre-compact message; usually correct, even if the lpu was always wrong, since the JSONL is append-only).
- Synthesize a visible system message in front of the boundary describing what happened. In my local implementation it's:
> 🔺 Orphaned compaction pointer (seam)
>
> The compactor referenced a chain predecessor uuid (<x>…) that was never persisted to disk (a Claude Code bug). Pre-compaction history above this notice has been reattached via the earlier message in the same file.
- Bookend the recovered region at the top of the chain with a matching marker, so the user can see exactly which span is reattached vs. natively-chained.
The recovered span isn't guaranteed to be the original chain (in pathological cases the messages above the boundary in the file could belong to a different conversation that landed in the same JSONL). The visible seam + bookend is what makes that ambiguity legible to the user instead of silently fudging the topology.
Reference implementation
Local implementation lives in claude-patches as Patch K (extension.js + webview/index.js):
extension.jsloader: after the existing chain-walk + cross-file resolution loop, scan for boundaries with unresolved lpu, splice in a synthetictype: "user"message with apfgk-seam-…uuid, rewrite the boundary's lpu to point at the synthetic, and prepend apfgk-bookend-…synthetic at the chain root.webview/index.jsuser-message renderer: detect thepfgk-uuid prefix and wrap the bubble with a colored<aside>(orange for seam, red for bookend) plus a click-to-scroll handler so the bookend-seam pair acts as a navigation bracket.
Verified on a 15 MB JSONL with one phantom-lpu boundary: the orphaned 2094-message span reattaches and renders, with the seam/bookend visibly marking the recovered region.
Screenshots
Bookend at the chain root and seam at the orphaned-region boundary. The recovered span (the original first prompt and ~2094 pre-compact messages) sits between them. Each acts as a click-to-scroll target for the other.
<img width="1440" height="905" alt="Patch K bookend at chain root" src="https://github.com/user-attachments/assets/a6b17a5c-3a53-455b-a54e-6c621e94d57b" />
<img width="1440" height="905" alt="Patch K seam at the orphaned-region boundary" src="https://github.com/user-attachments/assets/2989e33d-0b78-45d3-911d-ab62b5af7d14" />
Implementation note
The implementation isn't proposed for direct upstream adoption; it's stitched into an out-of-tree patchset and lives inside two splices in the bundle. The behavior is the request; how it's expressed in upstream code is your call.
Related
- #46603: write-side root cause (
compact.ts:598, fix in my comment there) - #49996: same dangling-lpu bug, different framing; closed
- #48937: read-side fallback to follow
logicalParentUuidwhenparentUuiddoesn't resolve (precondition for K, since K only fires when both fail) - #55700: 5 MB precompact-skip gating (chain walker can't even see content before the compact boundary for large files until that's bypassed)
Version
Observed on 2.1.121 / 2.1.123 / 2.1.126 (local Antigravity). Bug surface is unchanged across these.
---
Filed by Claude Code (Opus 4.7) on @ojura's machine after we wrapped up the write-side analysis on #46603. The implementation in claude-patches was iterated on live: instrument via CDP, observe the rendered DOM, refine the in-band-vs-out-of-band signaling, fix the placeholder filtering (isMeta:true was getting it caught by Sz4's render filter; out-of-band needed to be a uuid prefix). The "lost+found / fsck for sessions" framing came from @ojura.
This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗