From f77aaa62a85abe6b6fec1e9792c95b6f1a68f6f8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 10 Apr 2026 19:29:12 +0000 Subject: [PATCH] fix(context): make incremental GC purely node-based instead of token-based --- .../processors/historyTruncationProcessor.ts | 20 ++++++-- .../processors/rollingSummaryProcessor.ts | 51 ++++++++++--------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/core/src/context/processors/historyTruncationProcessor.ts b/packages/core/src/context/processors/historyTruncationProcessor.ts index 9c830dca6e..b0852160e3 100644 --- a/packages/core/src/context/processors/historyTruncationProcessor.ts +++ b/packages/core/src/context/processors/historyTruncationProcessor.ts @@ -23,13 +23,24 @@ export function createHistoryTruncationProcessor( id, name: 'HistoryTruncationProcessor', process: async ({ targets }: ProcessArgs) => { - // Calculate how many tokens we need to remove based on the configured knob - let targetTokensToRemove = 0; const strategy = options.target ?? 'max'; + const keptNodes: ConcreteNode[] = []; if (strategy === 'incremental') { - targetTokensToRemove = Infinity; - } else if (strategy === 'freeNTokens') { + // 'incremental' simply drops the single oldest node in the targets, ignoring tokens. + let removedNodes = 0; + for (const node of targets) { + if (removedNodes < 1) { + removedNodes++; + continue; + } + keptNodes.push(node); + } + return keptNodes; + } + + let targetTokensToRemove = 0; + if (strategy === 'freeNTokens') { targetTokensToRemove = options.freeTokensTarget ?? 0; if (targetTokensToRemove <= 0) return targets; } else if (strategy === 'max') { @@ -38,7 +49,6 @@ export function createHistoryTruncationProcessor( } let removedTokens = 0; - const keptNodes: ConcreteNode[] = []; // The targets are sequentially ordered from oldest to newest. // We want to delete the oldest targets first. diff --git a/packages/core/src/context/processors/rollingSummaryProcessor.ts b/packages/core/src/context/processors/rollingSummaryProcessor.ts index 21f57eb048..219519b5bc 100644 --- a/packages/core/src/context/processors/rollingSummaryProcessor.ts +++ b/packages/core/src/context/processors/rollingSummaryProcessor.ts @@ -66,35 +66,36 @@ export function createRollingSummaryProcessor( if (targets.length === 0) return targets; const strategy = options.target ?? 'max'; - let targetTokensToRemove = 0; - - if (strategy === 'incremental') { - // A rolling summary should target a small chunk. For now, since state isn't passed, - // we'll default to a fixed threshold, like 10000 tokens, to avoid eating the whole history. - // Ideally, the orchestrator should pass `tokensToRemove` explicitly. - targetTokensToRemove = 10000; - } else if (strategy === 'freeNTokens') { - targetTokensToRemove = options.freeTokensTarget ?? Infinity; - } else if (strategy === 'max') { - targetTokensToRemove = Infinity; - } - - if (targetTokensToRemove <= 0) return targets; - - let deficitAccumulator = 0; const nodesToSummarize: ConcreteNode[] = []; - // Scan oldest to newest to find the oldest block that exceeds the token requirement - for (const node of targets) { - if (node.id === targets[0].id && node.type === 'USER_PROMPT') { - // Keep system prompt if it's the very first node - continue; + if (strategy === 'incremental') { + // 'incremental' simply summarizes the minimum viable chunk (the oldest 2 nodes), ignoring token math. + for (const node of targets) { + if (node.id === targets[0].id && node.type === 'USER_PROMPT') { + continue; // Keep system prompt + } + nodesToSummarize.push(node); + if (nodesToSummarize.length >= 2) break; // We have enough for a minimum rolling summary + } + } else { + let targetTokensToRemove = 0; + if (strategy === 'freeNTokens') { + targetTokensToRemove = options.freeTokensTarget ?? Infinity; + } else if (strategy === 'max') { + targetTokensToRemove = Infinity; } - nodesToSummarize.push(node); - deficitAccumulator += env.tokenCalculator.getTokenCost(node); - - if (deficitAccumulator >= targetTokensToRemove) break; + if (targetTokensToRemove > 0) { + let deficitAccumulator = 0; + for (const node of targets) { + if (node.id === targets[0].id && node.type === 'USER_PROMPT') { + continue; // Keep system prompt + } + nodesToSummarize.push(node); + deficitAccumulator += env.tokenCalculator.getTokenCost(node); + if (deficitAccumulator >= targetTokensToRemove) break; + } + } } if (nodesToSummarize.length < 2) return targets; // Not enough context to summarize