fix: replay a compaction summary by its flag, not its framing (#1536)

Fixes #1535

A compaction summary on replay was only sent as a `compaction_update` if
its text also parsed, so a change in Claude Code's framing turned it
into a `user_message_chunk`, shown as text the user typed. That is what
happens to summaries from Claude Code 2.1.312 and 2.1.316.

`isCompactSummary` now decides on its own. A summary whose framing isn't
recognized is sent as a completed `compaction_update` without summary
text. This reverses the choice to keep such text visible: the
model-facing framing isn't kept as a summary either way, and showing it
as the user's words is the worse failure.

`PERSISTED_PREAMBLE` also allows content before the `Summary:` line, so
current summaries keep their text. All 31 summaries in my own
transcripts now parse, and a real `session/load` on one from 2.1.316
sends a `compaction_update` with its text and no summary
`user_message_chunk`.
This commit is contained in:
Hussain Hammad
2026-11-07 01:09:30 +03:00
committed by GitHub
parent 54392e144d
commit aa0c1234b5
4 changed files with 29 additions and 16 deletions

View File

@@ -10051,9 +10051,10 @@ export class ClaudeAcpAgent {
// The entity is keyed by the summary message's uuid, which differs from
// the live id (the `compacting` status uuid) — replay is terminal-first
// by the RFD, and no client merges a replay into a live entity store.
// A subagent's own compaction summary is not the root session's, and a
// summary whose framing is unrecognized stays visible as transcript
// text rather than vanishing behind a summary-less entity.
// A subagent's own compaction summary is not the root session's. A summary
// whose framing is unrecognized is still this compaction, without summary
// text: isCompactSummary says what it is, and the framing is Claude Code's
// to change.
if (
replayCompactionUpdates &&
isCompactSummaryMessage(message) &&
@@ -10069,10 +10070,9 @@ export class ClaudeAcpAgent {
airClient: this.toolCallCapabilities.air.client,
},
);
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
await replayCompaction.finish(message.uuid, "completed");
return;
}
replayCompaction.recordSummary(assistantMessageText(message.message));
await replayCompaction.finish(message.uuid, "completed");
return;
}
for (const notification of toAcpNotifications(

View File

@@ -67,7 +67,7 @@ const ANALYSIS_BLOCKS = [
* it as the post-compaction user message. */
const CONTINUATION_PREAMBLE = /^\s*This session is being continued from a previous conversation/i;
const PERSISTED_PREAMBLE =
/^\s*This session is being continued from a previous conversation[^\n]*\n+Summary:[ \t]*\r?\n/;
/^\s*This session is being continued from a previous conversation[^\n]*\n(?:[\s\S]*?\n)?Summary:[ \t]*\r?\n/;
const PERSISTED_TRAILERS = [
/\n+If you need specific details from before compaction[\s\S]*$/,
/\n+(?:Please )?[Cc]ontinue the conversation from where it left (?:it )?off[\s\S]*$/,

View File

@@ -18610,8 +18610,8 @@ describe("assembled assistant text fallback", () => {
]);
expect(child.some((update) => update.sessionUpdate === "compaction_update")).toBe(false);
// Unrecognized framing: the text stays visible rather than vanishing
// behind a summary-less entity.
// Unrecognized framing: still the compaction isCompactSummary says it is,
// without summary text, and never a message from the user.
const unknownFraming = await replay(true, [
{
...summaryMessage("odd-summary"),
@@ -18622,12 +18622,14 @@ describe("assembled assistant text fallback", () => {
},
},
]);
expect(unknownFraming.some((update) => update.sessionUpdate === "compaction_update")).toBe(
false,
);
expect(unknownFraming.some((update) => update.sessionUpdate === "user_message_chunk")).toBe(
true,
);
expect(unknownFraming).toEqual([
{
sessionUpdate: "compaction_update",
compactionId: "odd-summary",
status: "completed",
_meta: { jetbrains: { air: { version: 1, contextCompaction: { version: 1 } } } },
},
]);
});
it("preserves the model response after multiple compactions in one turn", async () => {

View File

@@ -130,6 +130,17 @@ describe("compactionSummaryText", () => {
);
});
it("strips the framing when other content sits before the Summary line", () => {
expect(
compactionSummaryText(
"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n" +
"<system-reminder>\n<tools-used-before-summary>Read, Bash</tools-used-before-summary>\n</system-reminder>\n" +
"Summary:\n1. Primary Request and Intent:\n Count upward.\n\n" +
"Continue the conversation from where it left off without asking the user any further questions.",
),
).toBe("1. Primary Request and Intent:\n Count upward.");
});
it("strips the older tagged transcript framing", () => {
expect(
compactionSummaryText(