[Bug fix + FR] Read-side recovery for sessions with dangling logicalParentUuid (lost+found / fsck)

Resolved 💬 8 comments Opened May 3, 2026 by ojura Closed Jun 22, 2026

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 / --resume shows ~⅔ 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:

  1. Detect any compact_boundary whose logicalParentUuid references a uuid not present in the in-memory message map after sibling-file resolution.
  2. 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).
  3. 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.

  1. 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.js loader: after the existing chain-walk + cross-file resolution loop, scan for boundaries with unresolved lpu, splice in a synthetic type: "user" message with a pfgk-seam-… uuid, rewrite the boundary's lpu to point at the synthetic, and prepend a pfgk-bookend-… synthetic at the chain root.
  • webview/index.js user-message renderer: detect the pfgk- 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 logicalParentUuid when parentUuid doesn'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.

View original on GitHub ↗

This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗