Skip to content

Commit 7df5f70

Browse files
fix(agents): skip redundant partial compaction summarization (#61603) (thanks @neeravmakwana)
1 parent 177e238 commit 7df5f70

3 files changed

Lines changed: 108 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ Docs: https://docs.openclaw.ai
250250
- Plugins: suppress trust-warning noise during non-activating snapshot and CLI metadata loads. (#61427) Thanks @gumadeiras.
251251
- Agents/video generation: accept `agents.defaults.videoGenerationModel` in strict config validation and `openclaw config set/get`, so gateways using `video_generate` no longer fail to boot after enabling a video model.
252252
- Matrix/streaming: add a quiet preview mode for streamed Matrix replies, keep legacy `partial` preview-first behavior, and finalize quiet media captions correctly so previews stop notifying early without dropping final text semantics. (#61450) Thanks @gumadeiras.
253+
- Agents/compaction: skip redundant partial summarization when no messages were oversized, so the same transcript is not summarized twice after a full summarization failure. Fixes #61465. (#61603) Thanks @neeravmakwana.
253254
- Gateway/shutdown: bound websocket-server shutdown even when no tracked clients remain, so gateway restarts stop hanging until the watchdog kills the process. (#61565) Thanks @mbelinky.
254255
- Control UI/multilingual: localize the remaining shared channel, instances, nodes, and gateway-confirmation strings so the dashboard stops mixing translated UI with hardcoded English labels. Thanks @vincentkoc.
255256
- Discord/media: raise the default inbound and outbound media cap to `100MB` so Discord matches Telegram more closely and larger attachments stop failing on the old low default.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
2+
import type { UserMessage } from "@mariozechner/pi-ai";
3+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
6+
const piCodingAgentMocks = vi.hoisted(() => ({
7+
generateSummary: vi.fn(),
8+
estimateTokens: vi.fn((_message: unknown) => 100),
9+
}));
10+
11+
vi.mock("@mariozechner/pi-coding-agent", async () => {
12+
const actual = await vi.importActual<typeof import("@mariozechner/pi-coding-agent")>(
13+
"@mariozechner/pi-coding-agent",
14+
);
15+
return {
16+
...actual,
17+
generateSummary: piCodingAgentMocks.generateSummary,
18+
estimateTokens: piCodingAgentMocks.estimateTokens,
19+
};
20+
});
21+
22+
const { summarizeWithFallback } = await import("./compaction.js");
23+
24+
const testModel = {
25+
id: "test",
26+
name: "test",
27+
contextWindow: 200_000,
28+
contextTokens: 200_000,
29+
maxTokens: 8192,
30+
} as unknown as NonNullable<ExtensionContext["model"]>;
31+
32+
describe("summarizeWithFallback", () => {
33+
beforeEach(() => {
34+
piCodingAgentMocks.generateSummary.mockReset();
35+
piCodingAgentMocks.generateSummary.mockRejectedValue(
36+
new Error("Summarization failed: fetch failed"),
37+
);
38+
piCodingAgentMocks.estimateTokens.mockReset();
39+
piCodingAgentMocks.estimateTokens.mockImplementation(() => 100);
40+
});
41+
42+
it("does not duplicate summarization when no messages were oversized", async () => {
43+
const messages: AgentMessage[] = [
44+
{
45+
role: "user",
46+
content: "hello",
47+
timestamp: 1,
48+
} satisfies UserMessage,
49+
];
50+
51+
const result = await summarizeWithFallback({
52+
messages,
53+
model: testModel,
54+
apiKey: "test-key", // pragma: allowlist secret
55+
signal: new AbortController().signal,
56+
reserveTokens: 1000,
57+
maxChunkTokens: 50_000,
58+
contextWindow: 200_000,
59+
});
60+
61+
expect(result).toContain("Context contained 1 messages");
62+
expect(result).toContain("0 oversized");
63+
// Full path: retryAsync attempts (3) for a single chunk; partial path must not run.
64+
expect(piCodingAgentMocks.generateSummary).toHaveBeenCalledTimes(3);
65+
});
66+
67+
it("still attempts partial summarization when oversized messages were excluded", async () => {
68+
piCodingAgentMocks.estimateTokens.mockImplementation((message: unknown) => {
69+
const content =
70+
typeof (message as { content?: unknown }).content === "string"
71+
? (message as { content: string }).content
72+
: "";
73+
return content.length > 10_000 ? 500_000 : 100;
74+
});
75+
76+
const messages: AgentMessage[] = [
77+
{
78+
role: "user",
79+
content: "small",
80+
timestamp: 1,
81+
} satisfies UserMessage,
82+
{
83+
role: "user",
84+
content: "x".repeat(500_000),
85+
timestamp: 2,
86+
} satisfies UserMessage,
87+
];
88+
89+
const result = await summarizeWithFallback({
90+
messages,
91+
model: testModel,
92+
apiKey: "test-key", // pragma: allowlist secret
93+
signal: new AbortController().signal,
94+
reserveTokens: 1000,
95+
maxChunkTokens: 50_000,
96+
contextWindow: 200_000,
97+
});
98+
99+
expect(result).toContain("2 messages (1 oversized)");
100+
// Full attempt (3 retries) plus distinct partial transcript (3 retries).
101+
expect(piCodingAgentMocks.generateSummary.mock.calls.length).toBe(6);
102+
});
103+
});

src/agents/compaction.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export async function summarizeWithFallback(params: {
398398
return await summarizeChunks(params);
399399
} catch (fullError) {
400400
log.warn(
401-
`Full summarization failed, trying partial: ${
401+
`Full summarization failed: ${
402402
fullError instanceof Error ? fullError.message : String(fullError)
403403
}`,
404404
);
@@ -420,7 +420,9 @@ export async function summarizeWithFallback(params: {
420420
}
421421
}
422422

423-
if (smallMessages.length > 0) {
423+
// When nothing was oversized, `smallMessages` is the same transcript as the full attempt.
424+
// Re-summarizing it would duplicate the same failing API work (and duplicate warn logs).
425+
if (smallMessages.length > 0 && smallMessages.length !== messages.length) {
424426
try {
425427
const partialSummary = await summarizeChunks({
426428
...params,

0 commit comments

Comments
 (0)