Skip to content

[BUG] Second /compact in one process re-appends the history the first one summarized away, transcript grows quadratically #92089

Description

@thomasbachem

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report
  • I am using the latest version of Claude Code

What's Wrong?

The first /compact in a Claude Code process writes its compact_boundary and summary rows and nothing else. A later /compact in the same process first re-appends, to the session .jsonl, a copy of every message the process has held from where its history began (the transcript start, or the boundary it resumed from) up to the row before the previous boundary's preservedSegment.headUuid. The copies keep their original uuid, parentUuid and timestamp.

The finished turn's stop_hook_summary row is then written with parentUuid pointing at the last copied row instead of the assistant message it follows, and the new boundary's logicalParentUuid and preservedSegment.tailUuid point at that hook row while preservedSegment.headUuid points at the real last assistant message, thousands of rows earlier in a long file.

Two effects:

  • The transcript grows quadratically with the number of compactions, since each replay repeats everything before the previous boundary. One desktop session of mine is 166 MB and 17,234 rows, of which 7,394 rows (104 MB) are these copies
  • Following parentUuid from the newest row leads through the replayed copy and around every message written between the previous compaction and this one. Readers that honor preservedMessages re-stitch the chain, readers that don't see that stretch as a dead branch

In the 166 MB case the Claude desktop app logged transcript is 165736911 bytes; tail-loading last 52428800 bytes and its conversation view ended at the last message before the compaction, with a day of later turns missing, until I relaunched the app. I assume that is a downstream effect of the two points above.

Not the same as #78592, which was about per-edit file content. The duplicate rows here are whole message rows tied to compaction.

What Should Happen?

A /compact appends the boundary and the summary. Rows that are already in the file are not written again, and the stop_hook_summary row's parentUuid is the message it follows.

Error Messages/Logs

Transcript written by the script below, uuids shortened, bookkeeping rows omitted

line  3  user                       uuid=1318e849 parentUuid=-
line  9  assistant                  uuid=a5ef5047 parentUuid=0b91f309
line 10  assistant                  uuid=2e4578cf parentUuid=a5ef5047
line 13  system stop_hook_summary   uuid=0d7a39b1 parentUuid=2e4578cf
line 14  user                       uuid=9f124e88 parentUuid=0d7a39b1
line 15  attachment                 uuid=bea57c1f parentUuid=9f124e88
line 16  assistant                  uuid=adafd410 parentUuid=bea57c1f   <- head of the first boundary
line 17  assistant                  uuid=3056d6fb parentUuid=adafd410
line 22  system stop_hook_summary   uuid=410bf7fa parentUuid=3056d6fb
line 23  system compact_boundary    uuid=456f5f65 logicalParentUuid=410bf7fa preservedSegment{head=adafd410 tail=410bf7fa}
line 24  user (isCompactSummary)    uuid=5b264742 parentUuid=456f5f65
...      two more turns
line 41  assistant                  uuid=2122c90c parentUuid=4d78410b   <- last message before the second /compact
line 42  assistant                  uuid=2503f4e8 parentUuid=2122c90c
line 47  user                       uuid=1318e849 parentUuid=-          <- copy of line 3
line 48  attachment                 uuid=e68f0a35 parentUuid=1318e849   <- copy of line 4
...      copies of lines 5 to 14
line 56  attachment                 uuid=bea57c1f parentUuid=9f124e88   <- copy of line 15, the row before the first boundary's head
line 57  system stop_hook_summary   uuid=cdf60317 parentUuid=bea57c1f   <- new row, parent is the copy above, not 2503f4e8
line 58  system compact_boundary    uuid=334cf755 logicalParentUuid=cdf60317 preservedSegment{head=2122c90c tail=cdf60317}
line 59  user (isCompactSummary)    uuid=ed9a72bc parentUuid=334cf755

Steps to Reproduce

  1. Save the script below as repro.py and run it in an empty directory with claude on PATH. I used the binary the desktop app ships, ~/Library/Application Support/Claude/claude-code/2.1.260/claude.app/Contents/MacOS/claude
  2. It drives one claude -p process through two short turns, /compact, two more turns and a second /compact, then prints every row of the session transcript whose uuid already appeared earlier in the file
  3. Expected output is nothing. Actual output is the ten rows before the first boundary's preservedSegment.headUuid, listed a second time
import json, os, subprocess, time, uuid

session = str(uuid.uuid4())
proc = subprocess.Popen(
    ['claude', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--model', 'haiku', '--session-id', session],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)

def send(text, until):
    proc.stdin.write(json.dumps({'type': 'user', 'message': {'role': 'user', 'content': text}}) + '\n')
    proc.stdin.flush()
    for line in proc.stdout:
        message = json.loads(line)
        if message.get('type') in until or message.get('subtype') in until:
            return

for step in ['Reply with one word: alpha', 'Reply with one word: beta', '/compact', 'Reply with one word: gamma', 'Reply with one word: delta', '/compact']:
    send(step, ('result', 'compact_boundary'))
    time.sleep(2)
proc.stdin.close()
proc.wait()

path = os.path.expanduser('~/.claude/projects/' + os.getcwd().replace('/', '-') + '/' + session + '.jsonl')
rows = [json.loads(line) for line in open(path)]
seen = {}
for index, row in enumerate(rows):
    if row.get('uuid') in seen:
        print('line %d duplicates line %d: %s uuid=%s' % (index + 1, seen[row['uuid']] + 1, row['type'], row['uuid'][:8]))
    elif row.get('uuid'):
        seen[row['uuid']] = index
line 47 duplicates line 3: user uuid=1318e849
line 48 duplicates line 4: attachment uuid=e68f0a35
line 49 duplicates line 5: attachment uuid=f405d3dc
line 50 duplicates line 6: attachment uuid=7e8e32e4
line 51 duplicates line 7: attachment uuid=0b91f309
line 52 duplicates line 9: assistant uuid=a5ef5047
line 53 duplicates line 10: assistant uuid=2e4578cf
line 54 duplicates line 13: system uuid=0d7a39b1
line 55 duplicates line 14: user uuid=9f124e88
line 56 duplicates line 15: attachment uuid=bea57c1f

The same happens in a process started with --resume on an existing session, the copy then starts at the boundary the process resumed from. My desktop app transcripts agree: all 21 compactions that were the first in their process are clean, and 13 of the 15 later ones replayed. I could not explain the 2 clean ones.

Claude Model

Not sure / Multiple models. The script uses haiku, my desktop transcripts show it on claude-opus-5, claude-fable-5 and claude-fable-5-1.

Is this a regression?

I don't know

Last Working Version

Unknown, the oldest replay I have is from 2026-08-24 on 2.1.237

Claude Code Version

2.1.260, also seen with 2.1.255, 2.1.247, 2.1.246 and 2.1.237

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other. I only use the Claude desktop app (1.46388.2), which drives its bundled CLI headless in stream-json mode, and the script runs that binary the same way. I have never used the interactive terminal.

Additional Information

Every compaction in my transcripts was a manual /compact, I have no auto-compaction data. The copied rows are exactly the process's in-memory history: rows on a rewound branch are not copied, and the copies come in memory order, a compaction's summary before its preserved messages, not file order.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:corebugSomething isn't workinghas reproHas detailed reproduction stepsplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions