Desktop app renders kept messages twice after a `session.compact` hook returns `{ messages }`; copies grow with every compaction

Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 0 comments · opened Sep 23, 2026

Environment

  • Claude desktop app (Code tab), Windows 11 Home 10.0.26200
  • Claude Code 2.1.280 (SDK process started by the app)
  • Plugin with a session.compact hook that returns { messages }, i.e. the kept messages with their engine handle, no summary message (fast-jev-compaction 0.3.0). A companion plugin queues /compact via $.command.run.

What happens
After each compaction, the engine appends the kept messages to the session .jsonl as new rows after the compact_boundary: new uuid, same timestamp, same message.id. The desktop app renders the whole file, so every kept user and assistant message appears once more after each compaction. After N compactions a message can show up N+1 times.

The model context is fine; only the transcript file and the UI are affected.

Evidence (from local transcripts, 23.09.2026)

  • One session with 3 compactions (2247, 710 and 405 ms): 127 user/assistant messages duplicated, up to 4 copies. For example, the same user prompt appears at rows 909 and 1122 with identical timestamp 2026-09-23T20:32:11.613Z, and the assistant reply at rows 928 and 1129 with the same message.id msg_011CfM1P4pFjdhHUJHVQ6SY3.
  • In long sessions the copies compound: up to 63 copies of one message, and compact_boundary rows get duplicated as well, so the file grows fast.
  • 25 sessions from the last 4 days: every session compacted by the hook has duplicates. The one session compacted only by the built-in summarizer (22968 ms, isCompactSummary) has 0.

Expected
Kept messages returned with their engine handle should be relinked or preserved, as with the built-in compaction's preserved segment, and not copied as new rows. Or the UI should not render messages that are before the latest boundary and also kept after it.

Repro

  1. Install a plugin with on('session.compact', async ($, e) => ({ messages: e.messages.slice(-20) })).
  2. In the desktop app, run a conversation and then /compact.
  3. The last 20 messages are shown twice. Run /compact again and they are shown three times.

View original on GitHub ↗