Compaction integrity bugs: dangling logicalParentUuid + title resolver prefers lastPrompt over firstPrompt
Two related bugs in how sessions are recorded and displayed across compaction boundaries. Version: 2.1.112 (VSCode/Antigravity extension).
Both observable across every auto-compacted session I have (20+ sessions in one project). Filing together because they're in the same neighbourhood of code (session recording + the lp / fetchSessions metadata parser) and because they interact with each other and with #48937.
---
Bug 1: Compaction stitch has a dangling logicalParentUuid
What happens
Every time a conversation auto-compacts (or /compact is invoked), the JSONL gets a "stitch" system message written with this shape:
{
"type": "system",
"content": "Conversation compacted",
"parentUuid": null,
"logicalParentUuid": "<some-uuid-X>",
"compactMetadata": { "trigger": "auto", "preTokens": 181304, ... }
}
The uuid <X> pointed to by logicalParentUuid never appears as a message's own uuid anywhere on disk.
Evidence
Reproducible trace in my project (~/.claude/projects/-home-juraj-phusion2/):
- Session
a9a7936a-621d-4329-b699-24a22ed6badb.jsonl: - L652 (last pre-compact message):
type=user, uuid=3b2a562e-..., timestamp=2026-04-08T14:02:56 - ~2-minute gap
- L655 (stitch):
type=system, uuid=4c4ea619-..., parentUuid=null, logicalParentUuid=84845ffd-d22a-45d4-bc3b-768e06b92bab, timestamp=2026-04-08T14:05:07 - Grep
84845ffdacross all 60+ JSONLs in the project: appears only aslogicalParentUuid, never asuuid. - The compaction subagent JSONL (
a9a7936a/subagents/agent-acompact-1947ebe65e892c99.jsonl) has its own line-1logicalParentUuid=84845ffd-...— same dangling uuid. - 20+ sibling sessions forked from this compaction moment all inherit the same dangling reference.
This pattern holds for every compaction boundary I've inspected across multiple sessions.
Why it's wrong
logicalParentUuid is documented (by its name and use in getTranscript) as a cross-boundary parent pointer. A dangling pointer violates the invariant that any uuid referenced in a JSONL resolves to a concrete persisted message.
Direct downstream consequence: the secondary-issue fix proposed in #48937 (make forkSession's getTranscript walk logicalParentUuid across compaction boundaries so forks contain the original pre-compact history) can't be implemented as stated — the pointer doesn't resolve, so the fork still wouldn't reach pre-compaction messages. Any tooling that walks the conversation graph across compaction boundaries (fork history, session linearization, transcript archaeology, the CLI's branch.ts-style rewrite) has to special-case this.
Expected
Either:
- (a) Persist the trigger/control message that currently has its uuid pre-allocated-but-never-flushed, OR
- (b) Set
logicalParentUuidon the stitch to the actual last pre-compact uuid (e.g.3b2a562e-...in the trace above).
(b) is cleaner — it keeps ephemeral /compact invocations out of the persisted transcript while still giving chain-walkers a resolvable pointer.
---
Bug 2: Title resolver prefers lastPrompt over firstPrompt, title drifts with message size
What happens
Session title (shown in sessions list / tab) is resolved via (minified, from lp(K,V,j) and the inline variant in fetchSessions):
z = customTitle || aiTitle || lastPrompt || summary || firstPrompt(head)
customTitle, aiTitle, lastPrompt, summary are tail-scanned (last 64KB). firstPrompt is head-scanned (first 64KB) via a different extractor (Jq4/gr).
Consequence: whether a session's title shows the original user prompt (stable identity) or the latest user prompt (drifts every turn) depends entirely on whether a last-prompt metadata entry happens to land in the tail 64KB at resolution time. That in turn depends on session size and how interleaved the metadata writes are — entirely an implementation detail the user has no control over.
Evidence
Three concrete sessions from the same project, all without custom-title or ai-title (so the fallback chain runs):
| session | total lines | last-prompt entries | in tail 64KB? | resolved title |
|---|---|---|---|---|
| a9a7936a-... | 1740 | 5 | no | "Let's start with @CLAUDE…" (original first prompt, stable) |
| 7c569713-... | 2478 | 51 | yes (3) | "I staged what I think should go into the commit, can you check?" (latest prompt, drifts) |
| 43c400a9-... | 2513 | 67 | — (has 91 custom-title in tail, customTitle wins) | "btw: btw you're sure it's not on the CLI side? …" |
The a9a7936a and 7c569713 difference is purely file-layout luck. Neither session did anything explicit to pick its title style.
Why it's wrong
- A session's "identity" in the sessions list should be stable. Users recognize conversations by what they started — not by what they most recently typed.
lastPromptchanges on every user message. It's a live-tail signal, not an identity.- The current ordering is silently non-deterministic from the user's perspective: two sessions with similar usage but different sizes resolve to completely different kinds of titles, for no principled reason — just which side of the 64KB tail window the metadata landed on.
Interaction with #48937
The primary fix for #48937 (append a last-prompt metadata entry on fork so the session becomes discoverable by listSessions) is correct and necessary. But it also causes Bug 2's drift on any session that has enough last-prompt entries to saturate the tail 64KB. The two fixes for discovery and for title-stability should be separated: last-prompt is good discovery metadata; it's bad title data.
Expected
Either:
- (a) Reorder the priority chain so
firstPromptwins overlastPrompt:
customTitle || aiTitle || firstPrompt(head) || summary || lastPrompt
- (b) Remove
lastPromptfrom the title chain entirely. It's more of a "last activity" / tooltip-level signal than a title.
Either gives deterministic, size-independent behavior.
---
How to verify
For Bug 1: take any auto-compacted session's JSONL, find the compact stitch (type:"system", parentUuid:null, logicalParentUuid set), grep that logicalParentUuid across all JSONLs — it never resolves to a real uuid.
For Bug 2: open two sessions without custom-title/ai-title entries, one small (last-prompt entries NOT in tail 64KB) and one larger (last-prompt entries IN tail 64KB). First shows original prompt as title; second shows latest prompt as title. No user action distinguishes them.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗