fix(compaction): preserve summary conversation anchors (#112755)

Co-authored-by: rune-dandelion-cult <rune.dandelion.cult@hotmail.com>
This commit is contained in:
Peter Steinberger
2026-07-22 14:15:32 -07:00
committed by GitHub
parent fd461d423c
commit 267d9f89ef
3 changed files with 97 additions and 6 deletions
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import {
hasMeaningfulConversationContent,
isRealConversationMessage,
} from "./compaction-real-conversation.js";
import type { AgentMessage } from "./runtime/index.js";
type SummaryRole = "branchSummary" | "compactionSummary";
function summaryMessage(role: SummaryRole, summary: string): AgentMessage {
return { role, summary, timestamp: 1 } as AgentMessage;
}
describe("compaction real conversation classification", () => {
it.each<SummaryRole>(["branchSummary", "compactionSummary"])(
"treats non-empty %s messages as conversation anchors",
(role) => {
const summary = summaryMessage(role, "The user asked for a repository audit.");
const toolResult = {
role: "toolResult",
toolCallId: "call-1",
toolName: "exec",
content: [{ type: "text", text: "audit output" }],
} as AgentMessage;
const messages = [summary, toolResult];
expect(hasMeaningfulConversationContent(summary)).toBe(true);
expect(isRealConversationMessage(summary, messages, 0)).toBe(true);
expect(isRealConversationMessage(toolResult, messages, 1)).toBe(true);
},
);
it.each<SummaryRole>(["branchSummary", "compactionSummary"])(
"rejects blank %s messages",
(role) => {
const summary = summaryMessage(role, " ");
expect(hasMeaningfulConversationContent(summary)).toBe(false);
expect(isRealConversationMessage(summary, [summary], 0)).toBe(false);
},
);
it("rejects tool-call-only messages and orphan tool results", () => {
const toolCall = {
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "exec", arguments: {} }],
} as AgentMessage;
const orphanToolResult = {
role: "toolResult",
toolCallId: "call-1",
toolName: "exec",
content: [{ type: "text", text: "audit output" }],
} as AgentMessage;
expect(isRealConversationMessage(toolCall, [toolCall], 0)).toBe(false);
expect(isRealConversationMessage(orphanToolResult, [orphanToolResult], 0)).toBe(false);
});
});
+7 -6
View File
@@ -30,6 +30,10 @@ function hasMeaningfulText(text: string): boolean {
return true;
}
function isSummaryRole(role: unknown): boolean {
return role === "branchSummary" || role === "compactionSummary";
}
/** Returns whether a message has content worth preserving as conversation. */
export function hasMeaningfulConversationContent(message: AgentMessage): boolean {
if ((message as { role?: unknown }).role === "custom") {
@@ -49,7 +53,7 @@ export function hasMeaningfulConversationContent(message: AgentMessage): boolean
const output = typeof bash.output === "string" ? bash.output : "";
return hasMeaningfulText(`${command}\n${output}`);
}
if ((message as { role?: unknown }).role === "branchSummary") {
if (isSummaryRole((message as { role?: unknown }).role)) {
const summary = (message as { summary?: unknown }).summary;
return typeof summary === "string" && hasMeaningfulText(summary);
}
@@ -93,10 +97,7 @@ function hasMeaningfulMessageContent(content: unknown): boolean {
function isToolResultConversationAnchor(message: AgentMessage): boolean {
const role = (message as { role?: unknown }).role;
return (
(role === "user" ||
role === "custom" ||
role === "bashExecution" ||
role === "branchSummary") &&
(role === "user" || role === "custom" || role === "bashExecution" || isSummaryRole(role)) &&
hasMeaningfulConversationContent(message)
);
}
@@ -112,7 +113,7 @@ export function isRealConversationMessage(
message.role === "assistant" ||
message.role === "custom" ||
message.role === "bashExecution" ||
message.role === "branchSummary"
isSummaryRole(message.role)
) {
return hasMeaningfulConversationContent(message);
}
@@ -2170,6 +2170,38 @@ describe("compactEmbeddedAgentSessionDirect hooks", () => {
});
});
it("compacts an overflow transcript anchored by a compaction summary", async () => {
sessionMessages.splice(
0,
sessionMessages.length,
{
role: "compactionSummary",
summary: "The user asked for a long-running repository audit.",
timestamp: 1,
},
{
role: "assistant",
content: [{ type: "toolCall", id: "call-1", name: "exec", arguments: {} }],
timestamp: 2,
},
{
role: "toolResult",
toolCallId: "call-1",
toolName: "exec",
content: [{ type: "text", text: "audit output" }],
isError: false,
timestamp: 3,
},
);
const result = await compactEmbeddedAgentSessionDirect(
wrappedCompactionArgs({ trigger: "overflow" }),
);
expect(result).toMatchObject({ ok: true, compacted: true });
expect(sessionCompactImpl).toHaveBeenCalledOnce();
});
it("skips compaction when the transcript only contains boilerplate replies and tool output", () => {
const messages = [
{ role: "user", content: "<b>HEARTBEAT_OK</b>", timestamp: 1 },