From 6495bece3a354c642f6570ee0d691c8b7d8b76f0 Mon Sep 17 00:00:00 2001 From: Lebing Xie Date: Mon, 15 Jun 2026 16:37:07 +0800 Subject: [PATCH] fix compressor summary duplication --- src/compressor.test.ts | 32 ++++++++++++++++++++++++++++++++ src/compressor.ts | 13 ++++++------- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/compressor.test.ts b/src/compressor.test.ts index a85e5da..ec9b97a 100644 --- a/src/compressor.test.ts +++ b/src/compressor.test.ts @@ -89,6 +89,11 @@ function makeSummaryBlock(summaryText: string, round?: number): MessageBlock { return block; } +/** 统计子串出现次数,用于断言 P2 不会把同一段摘要重复拼接。 */ +function countOccurrences(text: string, needle: string): number { + return text.split(needle).length - 1; +} + // --------------------------------------------------------------------------- // P0 衰减压缩 // --------------------------------------------------------------------------- @@ -562,6 +567,33 @@ describe("compactHistory (P2)", () => { const content = (summaryBlock.user as { content: string }).content; // 第二次 summary 应该包含第一次 summary 的内容 expect(content).toContain("[Context Summary]"); + // 但不能同时从 lastSummary 闭包和 summary block 各拼一次。 + expect(countOccurrences(content, "User: q1")).toBe(1); + } + }); + + it("does not duplicate cached summary when raw history is compacted again", () => { + const compressor = createContextCompressor({ compactKeepRecent: 1 }); + + // prepareMessages 路径只压缩本次请求视图,不会把 summary 写回 history。 + // 因此第二次压缩时传入的仍是完整原始 history;如果再拼 lastSummary, + // q1/q2 会既来自闭包摘要,又来自原始 oldBlocks,造成重复膨胀。 + compressor.compactHistory([ + makeTextBlock("q1", "a1", 1), + makeTextBlock("q2", "a2", 2), + ]); + + const result = compressor.compactHistory([ + makeTextBlock("q1", "a1", 1), + makeTextBlock("q2", "a2", 2), + makeTextBlock("q3", "a3", 3), + ]); + + const summaryBlock = result.blocks[0]!; + if (summaryBlock.type === "summary") { + const content = (summaryBlock.user as { content: string }).content; + expect(countOccurrences(content, "User: q1")).toBe(1); + expect(countOccurrences(content, "User: q2")).toBe(1); } }); diff --git a/src/compressor.ts b/src/compressor.ts index 8233092..75c8113 100644 --- a/src/compressor.ts +++ b/src/compressor.ts @@ -345,12 +345,6 @@ export function createContextCompressor( // 按类型遍历旧块,提取关键信息构建摘要文本 const summaryLines: string[] = []; - // 如果之前已有摘要,先加入,实现多层摘要的级联压缩 - if (lastSummary) { - summaryLines.push(lastSummary); - summaryLines.push("---"); - } - for (const block of oldBlocks) { if (block.type === "text") { const userContent = block.user ? extractText(block.user) : ""; @@ -386,7 +380,12 @@ export function createContextCompressor( } }); } else if (block.type === "summary") { - // 之前的 summary 块:保留其文本,纳入新摘要 + // 之前的 summary 块:保留其文本,纳入新摘要。 + // 注意:lastSummary 只作为 getState() 的状态快照,不再作为摘要输入源。 + // compactHistory(blocks) 的语义是“总结本次传入的完整 block 视图”: + // - prepareMessages 路径不会写回 history,下一次传入的 blocks 仍包含原始旧消息; + // - recovery 写回路径会把旧摘要作为 summary block 放回 history。 + // 如果再额外拼闭包缓存,就会把同一段旧摘要重复写入新 summary。 summaryLines.push(extractText(block.user)); } }