[BUG] A compaction summary can refer to its own <analysis> block, which is stripped before the summary is used
The compaction prompt asks the model for "an <analysis> block followed by a <summary> block". The analysis is then deleted: replace(/<analysis>[\s\S]*?<\/analysis>/, ""). Nothing in the prompt tells the summary not to refer to the analysis. Occasionally it does ("per the design in the analysis"), and the resumed session is sent to content that no longer exists.
Stripping the analysis is reasonable. The narrow problem is that the summary may cite it.
What it looks like
Three summaries from real sessions (verbatim excerpts, each checked against the raw transcript):
| CC version | Compaction | Summary text | Effect |
|---|---|---|---|
| 2.1.281 | auto, subagent (a planner writing a series of ~33 plan files) | "Write plans 20–32 compactly, per the design in the analysis." and "…then 21–32, following the design recorded in the analysis." | After the boundary the subagent re-read its source documents to rebuild the design. Its first new Write was tool call #55 (an upper bound on the cost: some of those reads would have happened anyway) |
| 2.1.231 | manual, main session | "Ephemeral — will not survive the job. Full source is reproduced in the analysis block above." | The only copy of a temporary script's source was cited to the stripped block |
| 2.1.259 | manual, main session | "qw-selftest.sh: all re-pins listed in analysis item 4 and 9" | No pre-compaction message mentions "item 4/9". The list existed only in the stripped block |
How widespread
3 verified in 944 unique compactions from one user's local transcripts (a regex over summaries, then hand-checked: 7 hits, 3 true, 4 false positives that quote the compaction prompt or name a real document section). That is a detected minimum: paraphrased references ("the plan above", "as reasoned earlier") are not matched. Rare, but when the stripped content is a design, a list or a script, the cost is real.
Reproduction
Not deterministically reproducible, because the trigger is the summariser's own choice to cite its analysis. A controlled headless repro was run on 2.1.282 (Linux, fresh scratch git repo, no PreCompact hook or custom instruction reaching the summariser):
S=$(claude -p --model "$M" --output-format json "We are planning a Postgres schema migration for an orders service. Invent plausible, concrete schema details yourself ... write the complete design: 40 numbered steps. For EACH step give: (a) the exact SQL statement, (b) the exact rollback SQL, (c) the batch size and lock timeout to use, (d) the verification query. ... later steps will be implemented exactly as designed." | jq -r .session_id)
claude -p --resume "$S" "Implement steps 1 and 2 only, exactly as designed: write migrations/001.sql and migrations/002.sql ... Then stop; steps 3-40 come later."
claude -p --resume "$S" "/compact" # one trial used "/compact keep the summary brief"
# then read the isCompactSummary message from the session transcript
| Trials | Model | Design | Result |
|---|---|---|---|
| 2 | sonnet | 24 steps | summaries 8.8k–12.1k chars; no reference to the analysis |
| 3 | opus | 40 steps | summaries 9.9k–12.9k chars; no reference to the analysis |
| 3 | sonnet | 40 steps | summaries 9.1k–11.8k chars; no reference to the analysis |
| 1 | opus | 40 steps, /compact keep the summary brief | summary 5.9k chars; no reference to the analysis |
0 of 9. At the observed ~0.3% rate that is the expected outcome (about a 3% chance of at least one hit), so it neither confirms nor refutes the defect. The missing guard can be checked from the prompt text alone (below).
Where it comes from (bundle strings, minified names, for orientation only)
Present in the installed 2.1.282 native binary, and unchanged in 2.1.283 (npm latest, @anthropic-ai/claude-code-linux-x64):
- Prompt:
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points.andYour entire response must be plain text: an <analysis> block followed by a <summary> block. - Post-processing:
var rvt=/<analysis>[\s\S]*?<\/analysis>/ … function ego(e){let n=e;return n=n.replace(rvt,""), …(2.1.282 names).
Neither version's prompt says the analysis is discarded, or tells the summary not to refer to it.
Suggested fix
Cheapest first:
- Prompt: add to the summary instructions: "The
<analysis>block is discarded before the summary is used. The summary must be self-contained: never refer to the analysis; copy any design, list or code you need into the summary." - Post-process check: after stripping, if the summary matches a small pattern set (
the analysis,analysis block,analysis item), re-ask, or append the referenced analysis text. More robust than (1) alone, but more code.
Keeping the analysis block fixes it trivially, but defeats the reason for stripping it (size).
User-side mitigation until then: the same sentence as (1), supplied as compaction custom instructions (/compact <instructions>, or printed by a PreCompact hook so auto-compaction gets it too).
Related
- #96619 (mine): a subagent's compaction summary points at the parent's transcript file. That issue mentions the first row above in passing; the pointer is a different defect with a different fix site, so this one gets its own issue.