mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 17:24:58 +00:00
fix: cancel compaction instead of truncating history when summarization fails (#10711)
* fix: cancel compaction instead of truncating history when summarization fails
When the compaction safeguard cannot generate a summary (no model, no API
key, or LLM error), it previously returned a "Summary unavailable" fallback
string and still truncated history. This caused irreversible data loss -
older messages were discarded even though no meaningful summary was produced.
Now returns `{ cancel: true }` in all three failure paths so the framework
aborts compaction entirely and preserves the full conversation history.
Fixes #10332
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use deterministic timestamps in compaction safeguard tests
Replace Date.now() with fixed timestamp (0) in test data to prevent
nondeterministic behavior in snapshot-based or order-dependent tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Changelog: note compaction cancellation safeguard fix
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -388,22 +388,17 @@ describe("compaction-safeguard extension model fallback", () => {
|
||||
model: undefined, // ctx.model is undefined (simulates compact.ts workflow)
|
||||
sessionManager,
|
||||
modelRegistry: {
|
||||
getApiKey: getApiKeyMock, // No API key, should use fallback
|
||||
getApiKey: getApiKeyMock, // No API key should now cancel compaction
|
||||
},
|
||||
} as unknown as Partial<ExtensionContext>;
|
||||
|
||||
// Call the handler and wait for result
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const result = (await compactionHandler!(mockEvent, mockContext)) as {
|
||||
compaction?: { summary?: string; firstKeptEntryId?: string };
|
||||
cancel?: boolean;
|
||||
};
|
||||
const compactionResult = result?.compaction;
|
||||
|
||||
// Verify that compaction returned a result (even with null API key, should use fallback)
|
||||
expect(compactionResult).toBeDefined();
|
||||
expect(compactionResult?.summary).toBeDefined();
|
||||
expect(compactionResult?.summary).toContain("Summary unavailable");
|
||||
expect(compactionResult?.firstKeptEntryId).toBe("entry-1");
|
||||
expect(result).toEqual({ cancel: true });
|
||||
|
||||
// KEY ASSERTION: Prove the fallback path was exercised
|
||||
// The handler should have called getApiKey with runtime.model (via ctx.model ?? runtime?.model)
|
||||
@@ -414,7 +409,7 @@ describe("compaction-safeguard extension model fallback", () => {
|
||||
expect(retrieved?.model).toEqual(model);
|
||||
});
|
||||
|
||||
it("returns fallback summary when both ctx.model and runtime.model are undefined", async () => {
|
||||
it("cancels compaction when both ctx.model and runtime.model are undefined", async () => {
|
||||
const sessionManager = stubSessionManager();
|
||||
|
||||
// Do NOT set runtime.model (both ctx.model and runtime.model will be undefined)
|
||||
@@ -464,15 +459,10 @@ describe("compaction-safeguard extension model fallback", () => {
|
||||
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const result = (await compactionHandler!(mockEvent, mockContext)) as {
|
||||
compaction?: { summary?: string; firstKeptEntryId?: string };
|
||||
cancel?: boolean;
|
||||
};
|
||||
const compactionResult = result?.compaction;
|
||||
|
||||
expect(compactionResult).toBeDefined();
|
||||
expect(compactionResult?.summary).toBe(
|
||||
"Summary unavailable due to context limits. Older messages were truncated.",
|
||||
);
|
||||
expect(compactionResult?.firstKeptEntryId).toBe("entry-1");
|
||||
expect(result).toEqual({ cancel: true });
|
||||
|
||||
// Verify early return: getApiKey should NOT have been called when both models are missing
|
||||
expect(getApiKeyMock).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user