From e891b35c0031f0e794a4323832e2244c185c092d Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Thu, 9 Apr 2026 21:32:14 +0800 Subject: [PATCH] fix: delay automatic summary rollup until above threshold --- maintenance/hierarchical-summary.js | 14 +++- tests/summary-rollup-threshold.mjs | 115 ++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 tests/summary-rollup-threshold.mjs diff --git a/maintenance/hierarchical-summary.js b/maintenance/hierarchical-summary.js index f392122..23c7235 100644 --- a/maintenance/hierarchical-summary.js +++ b/maintenance/hierarchical-summary.js @@ -483,7 +483,8 @@ function buildRollupCandidateText(entries = []) { .join("\n"); } -function getFoldableSummaryGroup(graph, fanIn = 3) { +function getFoldableSummaryGroup(graph, fanIn = 3, options = {}) { + const requireExcess = options?.requireExcess === true; const activeEntries = getActiveSummaryEntries(graph); const byLevel = new Map(); for (const entry of activeEntries) { @@ -495,7 +496,7 @@ function getFoldableSummaryGroup(graph, fanIn = 3) { const sortedLevels = [...byLevel.keys()].sort((left, right) => left - right); for (const level of sortedLevels) { const entries = byLevel.get(level) || []; - if (entries.length >= fanIn) { + if (requireExcess ? entries.length > fanIn : entries.length >= fanIn) { return entries.slice(0, fanIn); } } @@ -510,12 +511,15 @@ export async function rollupSummaryFrontier({ } = {}) { normalizeGraphSummaryState(graph); const fanIn = clampInt(settings.summaryRollupFanIn, 3, 2, 10); + const requireExcess = force !== true; const createdEntries = []; let foldedCount = 0; while (true) { throwIfAborted(signal); - const candidates = getFoldableSummaryGroup(graph, fanIn); + const candidates = getFoldableSummaryGroup(graph, fanIn, { + requireExcess, + }); if (candidates.length < fanIn) { break; } @@ -616,7 +620,9 @@ export async function rollupSummaryFrontier({ skipped: createdEntries.length === 0, reason: createdEntries.length === 0 - ? `当前没有达到 ${fanIn} 条同层活跃总结的折叠候选` + ? requireExcess + ? `当前没有超过 ${fanIn} 条同层活跃总结的折叠候选` + : `当前没有达到 ${fanIn} 条同层活跃总结的折叠候选` : "", }; } diff --git a/tests/summary-rollup-threshold.mjs b/tests/summary-rollup-threshold.mjs new file mode 100644 index 0000000..88ae2ec --- /dev/null +++ b/tests/summary-rollup-threshold.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { registerHooks } from "node:module"; + +const extensionsShimSource = [ + "export const extension_settings = {};", + "export function getContext() {", + " return {", + " chat: [],", + " chatMetadata: {},", + " extensionSettings: {},", + " powerUserSettings: {},", + " characters: {},", + " characterId: null,", + " name1: '',", + " name2: '',", + " chatId: 'test-chat',", + " };", + "}", +].join("\n"); + +const scriptShimSource = [ + "export function substituteParamsExtended(value) {", + " return String(value ?? '');", + "}", + "export function getRequestHeaders() {", + " return {};", + "}", +].join("\n"); + +const openAiShimSource = [ + "export const chat_completion_sources = { OPENAI: 'openai' };", + "export async function sendOpenAIRequest() {", + " throw new Error('sendOpenAIRequest should not be called in summary-rollup-threshold test');", + "}", +].join("\n"); + +registerHooks({ + resolve(specifier, context, nextResolve) { + if ( + specifier === "../../../extensions.js" || + specifier === "../../../../extensions.js" || + specifier === "../../../../../extensions.js" + ) { + return { + shortCircuit: true, + url: `data:text/javascript,${encodeURIComponent(extensionsShimSource)}`, + }; + } + if ( + specifier === "../../../../script.js" || + specifier === "../../../../../script.js" + ) { + return { + shortCircuit: true, + url: `data:text/javascript,${encodeURIComponent(scriptShimSource)}`, + }; + } + if ( + specifier === "../../../openai.js" || + specifier === "../../../../openai.js" + ) { + return { + shortCircuit: true, + url: `data:text/javascript,${encodeURIComponent(openAiShimSource)}`, + }; + } + return nextResolve(specifier, context); + }, +}); + +const { createEmptyGraph } = await import("../graph/graph.js"); +const { appendSummaryEntry } = await import("../graph/summary-state.js"); +const { rollupSummaryFrontier } = await import("../maintenance/hierarchical-summary.js"); + +const graph = createEmptyGraph(); + +appendSummaryEntry(graph, { + id: "summary-a", + level: 0, + kind: "small", + text: "第一条小总结", + messageRange: [1, 2], + extractionRange: [1, 1], +}); +appendSummaryEntry(graph, { + id: "summary-b", + level: 0, + kind: "small", + text: "第二条小总结", + messageRange: [3, 4], + extractionRange: [2, 2], +}); +appendSummaryEntry(graph, { + id: "summary-c", + level: 0, + kind: "small", + text: "第三条小总结", + messageRange: [5, 6], + extractionRange: [3, 3], +}); + +const result = await rollupSummaryFrontier({ + graph, + settings: { + summaryRollupFanIn: 3, + }, + force: false, +}); + +assert.equal(result.createdCount, 0); +assert.equal(result.foldedCount, 0); +assert.equal(result.skipped, true); +assert.match(String(result.reason || ""), /超过 3 条同层活跃总结/); + +console.log("summary-rollup-threshold tests passed");