mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-27 20:43:39 +00:00
fix(compaction): trim prefix when transcript ends in an oversized tool result (#95860)
findCutPoint defaulted cutIndex to the earliest valid cut (cutPoints[0], keep everything) and only moved it forward to a cut point at or after the backward token cursor. When the final entry is a toolResult whose estimate alone meets keepRecentTokens, the cursor stops at that trailing toolResult index, no valid cut point sits at or after it (toolResult entries are not valid cut points), and the default stuck at keep-everything. Compaction then summarized zero messages, so preflight and overflow compaction silently no-op and the session loops on a context it cannot shrink. Default cutIndex to the most recent valid cut before the forward search. When a cut point exists at or after the cursor the search still finds it and behavior is unchanged; only the trailing-tool-result case now keeps the recent tail and summarizes the prefix.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentMessage } from "../../types.js";
|
||||
import type { SessionTreeEntry } from "../types.js";
|
||||
import { estimateTokens, findCutPoint } from "./compaction.js";
|
||||
|
||||
const KEEP_RECENT_TOKENS = 20000;
|
||||
const LARGE_TOOL_OUTPUT = "x".repeat(120000);
|
||||
|
||||
function userText(text: string, timestamp: number): AgentMessage {
|
||||
return { role: "user", content: [{ type: "text", text }], timestamp };
|
||||
}
|
||||
|
||||
function assistantText(text: string, timestamp: number): AgentMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "claude-fable-5",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
function toolResultText(text: string, timestamp: number): AgentMessage {
|
||||
return {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-1",
|
||||
toolName: "bash",
|
||||
content: [{ type: "text", text }],
|
||||
isError: false,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
function messageEntry(message: AgentMessage, index: number): SessionTreeEntry {
|
||||
return {
|
||||
type: "message",
|
||||
id: `entry-${index}`,
|
||||
parentId: index === 0 ? null : `entry-${index - 1}`,
|
||||
timestamp: new Date(message.timestamp).toISOString(),
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTranscript(): SessionTreeEntry[] {
|
||||
const messages: AgentMessage[] = [
|
||||
userText("start of the conversation", 1),
|
||||
assistantText("first reply", 2),
|
||||
userText("please run the command", 3),
|
||||
assistantText("running it now", 4),
|
||||
toolResultText(LARGE_TOOL_OUTPUT, 5),
|
||||
];
|
||||
return messages.map((message, index) => messageEntry(message, index));
|
||||
}
|
||||
|
||||
describe("findCutPoint with a trailing oversized tool result", () => {
|
||||
it("counts the final tool result as larger than the keep budget", () => {
|
||||
const trailing = toolResultText(LARGE_TOOL_OUTPUT, 5);
|
||||
|
||||
expect(estimateTokens(trailing)).toBeGreaterThanOrEqual(KEEP_RECENT_TOKENS);
|
||||
});
|
||||
|
||||
it("trims the prefix instead of keeping the whole transcript", () => {
|
||||
const entries = buildTranscript();
|
||||
|
||||
const result = findCutPoint(entries, 0, entries.length, KEEP_RECENT_TOKENS);
|
||||
|
||||
expect(result.firstKeptEntryIndex).toBeGreaterThan(0);
|
||||
expect(result.firstKeptEntryIndex).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -407,6 +407,7 @@ export function findCutPoint(
|
||||
const messageTokens = estimateTokens(entry.message);
|
||||
accumulatedTokens += messageTokens;
|
||||
if (accumulatedTokens >= keepRecentTokens) {
|
||||
cutIndex = cutPoints[cutPoints.length - 1];
|
||||
for (const cutPoint of cutPoints) {
|
||||
if (cutPoint >= i) {
|
||||
cutIndex = cutPoint;
|
||||
|
||||
Reference in New Issue
Block a user