From 640d849108f29fac71865c8fe0fbeb72002bdbee Mon Sep 17 00:00:00 2001 From: Kaveh Mousavi Zamani Date: Thu, 5 Jun 2025 11:25:43 +0200 Subject: [PATCH] Adding a Disable automatic context clearing (Sliding Window) to Experimental settings More complex use of Roo might not tolerate any kind of cleaning. With recent model development, they can work autonomously for longer time which needs some kind of guranarty of context in some situations. --- packages/types/src/experiment.ts | 3 +- packages/types/src/global-settings.ts | 2 +- .../__tests__/sliding-window.test.ts | 95 +++++++++++++++++++ src/core/sliding-window/index.ts | 7 ++ src/core/task/Task.ts | 4 + src/shared/WebviewMessage.ts | 1 + src/shared/__tests__/experiments.test.ts | 3 + src/shared/experiments.ts | 2 + .../__tests__/ExtensionStateContext.test.tsx | 5 +- webview-ui/src/i18n/locales/en/settings.json | 4 + 10 files changed, 122 insertions(+), 4 deletions(-) diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 0e0db7276e..7b285e8c4e 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["powerSteering", "concurrentFileReads"] as const +export const experimentIds = ["powerSteering", "concurrentFileReads", "disableSlidingWindow"] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -19,6 +19,7 @@ export type ExperimentId = z.infer export const experimentsSchema = z.object({ powerSteering: z.boolean(), concurrentFileReads: z.boolean(), + disableSlidingWindow: z.boolean(), }) export type Experiments = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 899860be1c..a14206280a 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -48,7 +48,7 @@ export const globalSettingsSchema = z.object({ allowedMaxRequests: z.number().nullish(), autoCondenseContext: z.boolean().optional(), autoCondenseContextPercent: z.number().optional(), - maxConcurrentFileReads: z.number().optional(), + maxConcurrentFileReads: z.number().optional(), browserToolEnabled: z.boolean().optional(), browserViewportSize: z.string().optional(), diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts index a26ad6b53e..c541258df8 100644 --- a/src/core/sliding-window/__tests__/sliding-window.test.ts +++ b/src/core/sliding-window/__tests__/sliding-window.test.ts @@ -263,6 +263,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -302,6 +303,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -335,6 +337,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -347,6 +350,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -366,6 +370,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -378,6 +383,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -412,6 +418,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -445,6 +452,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -471,6 +479,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -507,6 +516,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -552,6 +562,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: true, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -617,6 +628,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: true, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -662,6 +674,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 50, // This shouldn't matter since autoCondenseContext is false + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -717,6 +730,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: true, autoCondenseContextPercent: 50, // Set threshold to 50% - our tokens are at 60% + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -767,6 +781,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: true, autoCondenseContextPercent: 50, // Set threshold to 50% - our tokens are at 40% + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -785,6 +800,78 @@ describe("Sliding Window", () => { // Clean up summarizeSpy.mockRestore() }) + + it("should not truncate when disableSlidingWindow is true even if tokens exceed threshold", async () => { + const modelInfo = createModelInfo(100000, 30000) + const totalTokens = 80001 // Well above threshold + + // Create messages with very small content in the last one to avoid token overflow + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await truncateConversationIfNeeded({ + messages: messagesWithSmallContent, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + apiHandler: mockApiHandler, + autoCondenseContext: false, + autoCondenseContextPercent: 100, + disableSlidingWindow: true, // This should prevent any truncation + systemPrompt: "System prompt", + taskId, + }) + + // Should return original messages without any truncation + expect(result).toEqual({ + messages: messagesWithSmallContent, + summary: "", + cost: 0, + prevContextTokens: totalTokens, + }) + }) + + it("should not use summarizeConversation when disableSlidingWindow is true even with autoCondenseContext enabled", async () => { + // Reset any previous mock calls + jest.clearAllMocks() + const summarizeSpy = jest.spyOn(condenseModule, "summarizeConversation") + + const modelInfo = createModelInfo(100000, 30000) + const totalTokens = 80001 // Well above threshold + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + const result = await truncateConversationIfNeeded({ + messages: messagesWithSmallContent, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + apiHandler: mockApiHandler, + autoCondenseContext: true, // This would normally trigger summarization + autoCondenseContextPercent: 50, // Well below our 80% usage + disableSlidingWindow: true, // This should prevent any processing + systemPrompt: "System prompt", + taskId, + }) + + // Verify summarizeConversation was not called + expect(summarizeSpy).not.toHaveBeenCalled() + + // Should return original messages without any processing + expect(result).toEqual({ + messages: messagesWithSmallContent, + summary: "", + cost: 0, + prevContextTokens: totalTokens, + }) + + // Clean up + summarizeSpy.mockRestore() + }) }) /** @@ -827,6 +914,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -846,6 +934,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -876,6 +965,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -895,6 +985,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -924,6 +1015,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -938,6 +1030,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -965,6 +1058,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) @@ -979,6 +1073,7 @@ describe("Sliding Window", () => { apiHandler: mockApiHandler, autoCondenseContext: false, autoCondenseContextPercent: 100, + disableSlidingWindow: false, systemPrompt: "System prompt", taskId, }) diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index dc9eaf718d..8692ed1b04 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -70,6 +70,7 @@ type TruncateOptions = { apiHandler: ApiHandler autoCondenseContext: boolean autoCondenseContextPercent: number + disableSlidingWindow: boolean systemPrompt: string taskId: string customCondensingPrompt?: string @@ -93,6 +94,7 @@ export async function truncateConversationIfNeeded({ apiHandler, autoCondenseContext, autoCondenseContextPercent, + disableSlidingWindow, systemPrompt, taskId, customCondensingPrompt, @@ -113,6 +115,11 @@ export async function truncateConversationIfNeeded({ // Calculate total effective tokens (totalTokens never includes the last message) const prevContextTokens = totalTokens + lastMessageTokens + // If sliding window is disabled, return original messages without any truncation + if (disableSlidingWindow) { + return { messages, summary: "", cost, prevContextTokens, error } + } + // Calculate available tokens for conversation history // Truncate if we're within TOKEN_BUFFER_PERCENTAGE of the context window const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index fa814f0661..ea49defeef 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -8,6 +8,8 @@ import delay from "delay" import pWaitFor from "p-wait-for" import { serializeError } from "serialize-error" +import { experiments, EXPERIMENT_IDS, experimentDefault } from "../../shared/experiments" + import { type ProviderSettings, type TokenUsage, @@ -1607,6 +1609,7 @@ export class Task extends EventEmitter { mode, autoCondenseContext = true, autoCondenseContextPercent = 100, + experiments: experimentsConfig = {}, } = state ?? {} // Get condensing configuration for automatic triggers @@ -1677,6 +1680,7 @@ export class Task extends EventEmitter { apiHandler: this.api, autoCondenseContext, autoCondenseContextPercent, + disableSlidingWindow: experiments.isEnabled({ ...experimentDefault, ...experimentsConfig }, EXPERIMENT_IDS.DISABLE_SLIDING_WINDOW), systemPrompt, taskId: this.taskId, customCondensingPrompt, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 05136c18b3..11ecc33c2b 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -62,6 +62,7 @@ export interface WebviewMessage { | "alwaysAllowSubtasks" | "autoCondenseContext" | "autoCondenseContextPercent" + | "disableSlidingWindow" | "condensingApiConfigId" | "updateCondensingPrompt" | "playSound" diff --git a/src/shared/__tests__/experiments.test.ts b/src/shared/__tests__/experiments.test.ts index e50bb18878..e95edfc8e8 100644 --- a/src/shared/__tests__/experiments.test.ts +++ b/src/shared/__tests__/experiments.test.ts @@ -19,6 +19,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, concurrentFileReads: false, + disableSlidingWindow: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -27,6 +28,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: true, concurrentFileReads: false, + disableSlidingWindow: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -35,6 +37,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, concurrentFileReads: false, + disableSlidingWindow: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index e387f7ddcd..585e111f99 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId } from "@roo-code/ export const EXPERIMENT_IDS = { POWER_STEERING: "powerSteering", CONCURRENT_FILE_READS: "concurrentFileReads", + DISABLE_SLIDING_WINDOW: "disableSlidingWindow", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -16,6 +17,7 @@ interface ExperimentConfig { export const experimentConfigsMap: Record = { POWER_STEERING: { enabled: false }, CONCURRENT_FILE_READS: { enabled: false }, + DISABLE_SLIDING_WINDOW: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index 30f5fc7111..2a3c86f6e8 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -194,7 +194,7 @@ describe("mergeExtensionState", () => { writeDelayMs: 1000, requestDelaySeconds: 5, mode: "default", - experiments: {} as Record, + experiments: { disableSlidingWindow: false } as Record, customModes: [], maxOpenTabsContext: 20, maxWorkspaceFiles: 100, @@ -213,7 +213,7 @@ describe("mergeExtensionState", () => { const prevState: ExtensionState = { ...baseState, apiConfiguration: { modelMaxTokens: 1234, modelMaxThinkingTokens: 123 }, - experiments: {} as Record, + experiments: { disableSlidingWindow: false } as Record, } const newState: ExtensionState = { @@ -223,6 +223,7 @@ describe("mergeExtensionState", () => { powerSteering: true, autoCondenseContext: true, concurrentFileReads: true, + disableSlidingWindow: false, } as Record, } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 4aaf873cb2..93db46e23f 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -492,6 +492,10 @@ "MULTI_SEARCH_AND_REPLACE": { "name": "Use experimental multi block diff tool", "description": "When enabled, Roo will use multi block diff tool. This will try to update multiple code blocks in the file in one request." + }, + "DISABLE_SLIDING_WINDOW": { + "name": "Disable automatic context clearing (Sliding Window)", + "description": "When enabled, Roo will not automatically clear or condense the conversation context when it grows beyond the model's limit. This preserves the full history but may lead to API errors if the context becomes too large." } }, "promptCaching": {