Resume merges transcript lines sharing an assistant message.id across compact boundaries, multiplying messages kept by a session.compact hook
What happens
A session.compact function hook that keeps messages by returning them with their handle (no summary) makes the host write each kept message as a new transcript line with the same message.id as the original. Each compaction adds one more line per kept message.
After a resume, the transcript is rebuilt and every line sharing an assistant message.id is merged into one message, including lines from before earlier compact_boundary entries. The next compaction receives the merged message (its text repeated N times) and writes N copies.
Evidence
Claude Code 2.1.286 (claude-desktop), function hooks enabled, compaction plugin fast-jev-compaction.
| event | dup copies |
|---|---|
| compact #1, compact #2 (same process) | none |
| SessionStart:resume | – |
| compact #3 | 20 assistant messages kept 2–3 times |
For all 22 affected messages, the number of copies after compact #3 equals the number of lines carrying that message.id before it. Reproduced on a fork (claude -p --resume <id> --fork-session "/compact"): all 26 pre-existing ids again matched exactly (3 lines → now 6 copies). preTokens of compact #3 was 160.8K while /context just before it reported 130.4K.
Expected
Merging by message.id should stop at a compact_boundary (or only merge consecutive lines of the current chain), so messages a hook keeps with their handle are not multiplied after resume.
Workaround
The hook drops the handle on kept assistant messages so the host rebuilds them with a fresh id. Thinking blocks of those messages are lost.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗