Skip to content

Commit 24626e5

Browse files
authored
fix(compaction): count bashExecution and summary turns in pre-prompt overflow precheck (openclaw#97861)
estimateMessageTokenPressure special-cased toolResult, tool, and assistant roles and otherwise read record.content. bashExecution stores its payload in command/output and branchSummary/compactionSummary store theirs in summary, so record.content was undefined and those turns scored as bare boundary overhead. The provider request expands them via convertToLlm into full user text, so bash-heavy sessions skipped the preflight overflow gate and submitted oversized prompts. Estimate each affected role from the exact text convertToLlm renders: bashExecutionToText for bash turns (zero for excludeFromContext records, which convertToLlm drops), and the summary prefix/suffix plus summary text for the two summary roles.
1 parent 888f399 commit 24626e5

2 files changed

Lines changed: 117 additions & 1 deletion

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
2+
import { describe, expect, it } from "vitest";
3+
import { convertToLlm } from "../../../../packages/agent-core/src/harness/messages.js";
4+
import {
5+
estimateLlmBoundaryTokenPressure,
6+
shouldPreemptivelyCompactBeforePrompt,
7+
} from "./preemptive-compaction.js";
8+
9+
const BIG_OUTPUT = "build log line ".repeat(90000);
10+
11+
function bashExecMessage(output = BIG_OUTPUT): AgentMessage {
12+
return {
13+
role: "bashExecution",
14+
command: "npm run build",
15+
output,
16+
exitCode: 0,
17+
cancelled: false,
18+
truncated: false,
19+
timestamp: 1,
20+
} as unknown as AgentMessage;
21+
}
22+
23+
function compactionSummaryMessage(summary: string): AgentMessage {
24+
return {
25+
role: "compactionSummary",
26+
summary,
27+
tokensBefore: 0,
28+
timestamp: 1,
29+
} as unknown as AgentMessage;
30+
}
31+
32+
function providerTokenApprox(message: AgentMessage): number {
33+
const [llm] = convertToLlm([message]);
34+
const content =
35+
llm && Array.isArray((llm as { content?: unknown }).content)
36+
? (llm as { content: { type: string; text?: string }[] }).content
37+
: [];
38+
const text = content
39+
.filter((b) => b.type === "text")
40+
.map((b) => b.text ?? "")
41+
.join("");
42+
return Math.ceil(text.length / 4);
43+
}
44+
45+
describe("preemptive precheck counts bashExecution and summary turns", () => {
46+
it("estimates a large bash turn near its provider-rendered size", () => {
47+
const msg = bashExecMessage();
48+
const realProviderTokens = providerTokenApprox(msg);
49+
const precheckTokens = estimateLlmBoundaryTokenPressure({ messages: [msg], prompt: "" });
50+
51+
expect(realProviderTokens).toBeGreaterThan(50000);
52+
expect(precheckTokens).toBeGreaterThanOrEqual(realProviderTokens);
53+
});
54+
55+
it("counts compactionSummary text instead of bare boundary overhead", () => {
56+
const msg = compactionSummaryMessage("recap ".repeat(20000));
57+
const realProviderTokens = providerTokenApprox(msg);
58+
const precheckTokens = estimateLlmBoundaryTokenPressure({ messages: [msg], prompt: "" });
59+
60+
expect(realProviderTokens).toBeGreaterThan(20000);
61+
expect(precheckTokens).toBeGreaterThanOrEqual(realProviderTokens);
62+
});
63+
64+
it("drops a bash turn excluded from context", () => {
65+
const excluded = {
66+
...(bashExecMessage() as unknown as Record<string, unknown>),
67+
excludeFromContext: true,
68+
} as unknown as AgentMessage;
69+
70+
expect(estimateLlmBoundaryTokenPressure({ messages: [excluded], prompt: "" })).toBeLessThan(50);
71+
});
72+
73+
it("routes an oversized bash transcript to compaction", () => {
74+
const decision = shouldPreemptivelyCompactBeforePrompt({
75+
messages: [bashExecMessage()],
76+
prompt: "continue",
77+
contextTokenBudget: 128000,
78+
reserveTokens: 16384,
79+
});
80+
81+
expect(decision.route).not.toBe("fits");
82+
expect(decision.shouldCompact).toBe(true);
83+
expect(decision.overflowTokens).toBeGreaterThan(0);
84+
});
85+
});

src/agents/embedded-agent-runner/run/preemptive-compaction.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import {
99
MIN_PROMPT_BUDGET_TOKENS,
1010
} from "../../agent-compaction-constants.js";
1111
import { SAFETY_MARGIN } from "../../compaction.js";
12-
import type { AgentMessage } from "../../runtime/index.js";
12+
import type { AgentMessage, BashExecutionMessage } from "../../runtime/index.js";
13+
import {
14+
BRANCH_SUMMARY_PREFIX,
15+
BRANCH_SUMMARY_SUFFIX,
16+
bashExecutionToText,
17+
COMPACTION_SUMMARY_PREFIX,
18+
COMPACTION_SUMMARY_SUFFIX,
19+
} from "../../runtime/index.js";
1320
import { estimateToolResultReductionPotential } from "../tool-result-truncation.js";
1421
import type { PreemptiveCompactionRoute } from "./preemptive-compaction.types.js";
1522

@@ -158,6 +165,30 @@ function estimateMessageTokenPressure(message: AgentMessage): number {
158165
return tokens;
159166
}
160167

168+
if (record.role === "bashExecution") {
169+
if (record.excludeFromContext === true) {
170+
return 0;
171+
}
172+
tokens += estimateStringTokenPressure(
173+
bashExecutionToText(record as unknown as BashExecutionMessage),
174+
);
175+
return tokens;
176+
}
177+
178+
if (record.role === "branchSummary") {
179+
const summary = typeof record.summary === "string" ? record.summary : "";
180+
tokens += estimateStringTokenPressure(BRANCH_SUMMARY_PREFIX + summary + BRANCH_SUMMARY_SUFFIX);
181+
return tokens;
182+
}
183+
184+
if (record.role === "compactionSummary") {
185+
const summary = typeof record.summary === "string" ? record.summary : "";
186+
tokens += estimateStringTokenPressure(
187+
COMPACTION_SUMMARY_PREFIX + summary + COMPACTION_SUMMARY_SUFFIX,
188+
);
189+
return tokens;
190+
}
191+
161192
if (record.role === "assistant") {
162193
const content = record.content;
163194
if (Array.isArray(content)) {

0 commit comments

Comments
 (0)