From 14505d0dfe69d9956c311c2da0e43c101f8ccf94 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 2 Oct 2025 21:32:11 +0000 Subject: [PATCH 1/3] fix: process queued messages after context condensing completes - Add processQueuedMessages() call after condenseContext() in Task.ts - Add processQueuedMessages() call after task.condenseContext() in ClineProvider.ts - This ensures messages queued during context condensing are sent after completion Fixes #8477 --- src/core/task/Task.ts | 3 +++ src/core/webview/ClineProvider.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 851df91e6c..615692f39b 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1051,6 +1051,9 @@ export class Task extends EventEmitter implements TaskLike { { isNonInteractive: true } /* options */, contextCondense, ) + + // Process any queued messages after condensing completes + this.processQueuedMessages() } async say( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2c20d0939c..ecf2742512 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1538,6 +1538,9 @@ export class ClineProvider } await task.condenseContext() await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) + + // Process any queued messages after condensing completes + task.processQueuedMessages() } // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder From 7c35be4c44ed30fdf5fc59e022dd0dd839afc978 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 2 Oct 2025 21:36:14 +0000 Subject: [PATCH 2/3] fix: remove duplicate processQueuedMessages call - Keep only the call in Task.condenseContext() - Remove redundant call from ClineProvider.condenseTaskContext() --- src/core/webview/ClineProvider.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ecf2742512..2c20d0939c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1538,9 +1538,6 @@ export class ClineProvider } await task.condenseContext() await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) - - // Process any queued messages after condensing completes - task.processQueuedMessages() } // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder From 7f458fc591c64193d1e03169b42d7f035f6297d4 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 3 Oct 2025 23:56:55 +0000 Subject: [PATCH 3/3] test(task): add queued message processing tests after condense; ensure cross-task isolation --- src/core/task/__tests__/Task.spec.ts | 133 +++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 116c78d760..df2cb84292 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -148,6 +148,18 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({ vi.mock("../../ignore/RooIgnoreController") +vi.mock("../../condense", async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + summarizeConversation: vi.fn().mockResolvedValue({ + messages: [{ role: "user", content: [{ type: "text", text: "continued" }], ts: Date.now() }], + summary: "summary", + cost: 0, + newContextTokens: 1, + }), + } +}) // Mock storagePathManager to prevent dynamic import issues. vi.mock("../../../utils/storage", () => ({ getTaskDirectoryPath: vi @@ -1777,3 +1789,124 @@ describe("Cline", () => { }) }) }) + +describe("Queued message processing after condense", () => { + function createProvider(): any { + const storageUri = { fsPath: path.join(os.tmpdir(), "test-storage") } + const ctx = { + globalState: { + get: vi.fn().mockImplementation((_key: keyof GlobalState) => undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, + globalStorageUri: storageUri, + workspaceState: { + get: vi.fn().mockImplementation((_key) => undefined), + update: vi.fn().mockResolvedValue(undefined), + keys: vi.fn().mockReturnValue([]), + }, + secrets: { + get: vi.fn().mockResolvedValue(undefined), + store: vi.fn().mockResolvedValue(undefined), + delete: vi.fn().mockResolvedValue(undefined), + }, + extensionUri: { fsPath: "/mock/extension/path" }, + extension: { packageJSON: { version: "1.0.0" } }, + } as unknown as vscode.ExtensionContext + + const output = { + appendLine: vi.fn(), + append: vi.fn(), + clear: vi.fn(), + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + } + + const provider = new ClineProvider(ctx, output as any, "sidebar", new ContextProxy(ctx)) as any + provider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) + provider.postStateToWebview = vi.fn().mockResolvedValue(undefined) + provider.getState = vi.fn().mockResolvedValue({}) + return provider + } + + const apiConfig: ProviderSettings = { + apiProvider: "anthropic", + apiModelId: "claude-3-5-sonnet-20241022", + apiKey: "test-api-key", + } as any + + it("processes queued message after condense completes", async () => { + const provider = createProvider() + const task = new Task({ + provider, + apiConfiguration: apiConfig, + task: "initial task", + startTask: false, + }) + + // Make condense fast + deterministic + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("system") + const submitSpy = vi.spyOn(task, "submitUserMessage").mockResolvedValue(undefined) + + // Queue a message during condensing + task.messageQueueService.addMessage("queued text", ["img1.png"]) + + // Use fake timers to capture setTimeout(0) in processQueuedMessages + vi.useFakeTimers() + await task.condenseContext() + + // Flush the microtask that submits the queued message + vi.runAllTimers() + vi.useRealTimers() + + expect(submitSpy).toHaveBeenCalledWith("queued text", ["img1.png"]) + expect(task.messageQueueService.isEmpty()).toBe(true) + }) + + it("does not cross-drain queues between separate tasks", async () => { + const providerA = createProvider() + const providerB = createProvider() + + const taskA = new Task({ + provider: providerA, + apiConfiguration: apiConfig, + task: "task A", + startTask: false, + }) + const taskB = new Task({ + provider: providerB, + apiConfiguration: apiConfig, + task: "task B", + startTask: false, + }) + + vi.spyOn(taskA as any, "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(taskB as any, "getSystemPrompt").mockResolvedValue("system") + + const spyA = vi.spyOn(taskA, "submitUserMessage").mockResolvedValue(undefined) + const spyB = vi.spyOn(taskB, "submitUserMessage").mockResolvedValue(undefined) + + taskA.messageQueueService.addMessage("A message") + taskB.messageQueueService.addMessage("B message") + + // Condense in task A should only drain A's queue + vi.useFakeTimers() + await taskA.condenseContext() + vi.runAllTimers() + vi.useRealTimers() + + expect(spyA).toHaveBeenCalledWith("A message", undefined) + expect(spyB).not.toHaveBeenCalled() + expect(taskB.messageQueueService.isEmpty()).toBe(false) + + // Now condense in task B should drain B's queue + vi.useFakeTimers() + await taskB.condenseContext() + vi.runAllTimers() + vi.useRealTimers() + + expect(spyB).toHaveBeenCalledWith("B message", undefined) + expect(taskB.messageQueueService.isEmpty()).toBe(true) + }) +})