mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(compaction): preserve summary conversation anchors (#112755)
Co-authored-by: rune-dandelion-cult <rune.dandelion.cult@hotmail.com>
This commit is contained in:
committed by
GitHub
parent
fd461d423c
commit
267d9f89ef
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user