[BUG] The second `/compact` in a process parents the transcript's continuation before the first compaction, orphaning every message in between

Status Closed — not planned
Reported on v2.1.260
Maintainer reply None cached
Activity 1 comment · opened Sep 8, 2026 · closed Sep 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Compact a session twice without the process restarting in between, and the second compaction parents the continuing row on a row from before the first compaction. Walking parentUuid back from the newest row then skips everything that happened between the two.

It reproduces in about five minutes on a throwaway session. Three runs of the same script, Haiku 4.5, transcripts of 64 to 80 rows:

  • two /compacts in one process: the continuing row is parented on an attachment written 25 s before the first compact_boundary instead of on the last message. 16 conversation rows are then unreachable from the newest row: the assistant's last reply before the first compaction, that compaction's summary and its /compact rows, and all three turns after it
  • same script, first /compact queued mid-turn rather than from idle: identical outcome, so how the compaction is invoked makes no difference
  • same script, process restarted with --resume between the compactions: the continuing row is parented on the actual last message, and nothing drops off the walk

The broken runs also re-emit rows. At the second compaction the process writes its earliest message rows out again, same uuids with slug, promptId and toolUseResult refreshed, and hangs the compaction turn off the end of that block, which stops just before the assistant's last reply preceding the first compaction. After a restart there is no such block.

The session where it bit me is a Desktop session with two compactions two hours and twenty minutes apart. Its continuing row, written at 12:25:37, is parented on a row from 10:05:12 rather than on the last message at 12:25:35, and 413 rows fell off the walk, 278 of them user and assistant turns.

Two things break on that chain. The Desktop scrollback jumps from before the first compaction straight to the newest messages, and Rewind stops working for the whole session: rewindV2 resolves the target's resume point against the same walk, lands on a row from 10:05:10, and the turn dies behind a "Claude couldn't process that message" card.

The model's context is unaffected. The second summary is complete and the session resumed on it at ~112k input tokens rather than the ~330k the raw chain would have carried, so the damage is confined to the transcript and to what reads it.

What Should Happen?

The continuing row should be parented on the last message before the compaction, the way it already is when the process restarts in between, so the walk never skips a stretch of the conversation.

Error Messages/Logs

2026-09-08 14:00:23 [info] LocalSessions.rewindV2: sessionId=local_4136cebc… target=3792774e…
2026-09-08 14:00:24 [info] [Rewind] resumeSessionAt=71ab79d3… + forkSession for session local_4136cebc…
2026-09-08 14:00:25 [warn] [CCD CycleHealth] local_4136cebc… cli_execution_error (error_during_execution): No message found with message.uuid of: 71ab79d3…
2026-09-08 14:00:26 [info] [CCD] Clearing stale pendingRewindTo 71ab79d3… for session local_4136cebc… — rewind target not on CLI's active chain

From ~/Library/Logs/Claude/main.log. The rewind target was a message from 14:00, 71ab79d3… is a row from 10:05:10, and the chain had been rewritten at 12:25:37, so the rewind exposed the damage rather than causing it.

Steps to Reproduce

  1. Start a session and send any prompt, so there is a turn to compact
  2. Run /compact
  3. Send two more short prompts, for instance Say the single word: alpha and then beta
  4. Run /compact a second time. The process must not have restarted in between, so run the steps in one sitting: the Desktop app restarts an idle session's process after 900 s
  5. Run the check below against ~/.claude/projects/<project>/<sessionId>.jsonl
import json, sys
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
pos = {r["uuid"]: i for i, r in enumerate(rows) if r.get("uuid")}
tip = [r for r in rows if r.get("uuid")][-1]
seen, cur = set(), tip["uuid"]
while cur in pos:
	seen.add(cur)
	cur = rows[pos[cur]].get("parentUuid")
lost = [r for r in rows if r.get("type") in ("user", "assistant") and r.get("uuid") not in seen]
print(len(lost), "conversation rows unreachable from the newest row")

It prints 16 for the broken run and 2 for the restarted one, where the only unreachable rows are the two summaries, which are branch roots by design. In the broken run the alpha and beta turns are among the missing.

I drove this through the CLI the Desktop app ships, with the app's own flags (--output-format stream-json --input-format stream-json --replay-user-messages), so the transcript writer is the same one, and the session where it bit me was an ordinary Desktop session. I have not tried the interactive terminal REPL and cannot say whether that path is affected.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.260 (Claude Code), bundled in Claude Desktop 1.46388.4

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Environment: the Claude Desktop app on macOS 26.5.1, Apple Silicon. The repro ran on Haiku 4.5, the affected session on Opus.

The Desktop app pins the CLI it runs, so the version above is 2.1.260 rather than the current npm 2.1.263. The same repro breaks on 2.1.258, the other copy the app keeps on disk, so it is not new in 2.1.260.

Related: #86752 is a different failure of the same pair of features, where a queued_command attachment breaks the Desktop chain walk and Rewind refuses the target instead of resolving it to an unreachable row.

Nothing is lost on disk. Pointing the second compact_boundary at the last message before it, and the row after it at that boundary's summary, puts all 413 rows of the affected session back on the walk.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗