From 28a4bc3b44559600e691189ac93d86ca55b4f941 Mon Sep 17 00:00:00 2001 From: wangdepeng Date: Mon, 2 Jun 2025 15:55:43 +0800 Subject: [PATCH 1/2] fix: async checkpoint initialization in background result to duplicate output in ChatView Signed-off-by: wangdepeng --- src/core/task/Task.ts | 65 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 46da7485ed..09d7db753d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -86,6 +86,9 @@ import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" +// Constants +export const DEFAULT_CKPT_INIT_TIMEOUT_MS = 10000 // 10 seconds + export type ClineEvents = { message: [{ action: "created" | "updated"; message: ClineMessage }] taskStarted: [] @@ -105,6 +108,7 @@ export type TaskOptions = { apiConfiguration: ProviderSettings enableDiff?: boolean enableCheckpoints?: boolean + checkpointInitTimeoutMs?: number fuzzyMatchThreshold?: number consecutiveMistakeLimit?: number task?: string @@ -186,6 +190,7 @@ export class Task extends EventEmitter { // Checkpoints enableCheckpoints: boolean + checkpointInitTimeoutMs: number checkpointService?: RepoPerTaskCheckpointService checkpointServiceInitializing = false @@ -207,6 +212,7 @@ export class Task extends EventEmitter { apiConfiguration, enableDiff = false, enableCheckpoints = true, + checkpointInitTimeoutMs = DEFAULT_CKPT_INIT_TIMEOUT_MS, fuzzyMatchThreshold = 1.0, consecutiveMistakeLimit = 3, task, @@ -252,6 +258,7 @@ export class Task extends EventEmitter { this.globalStoragePath = provider.context.globalStorageUri.fsPath this.diffViewProvider = new DiffViewProvider(this.cwd) this.enableCheckpoints = enableCheckpoints + this.checkpointInitTimeoutMs = checkpointInitTimeoutMs this.rootTask = rootTask this.parentTask = parentTask @@ -1108,8 +1115,8 @@ export class Task extends EventEmitter { // Task Loop private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise { - // Kicks off the checkpoints initialization process in the background. - getCheckpointService(this) + // Ensure checkpoint initialization completes before starting the main loop to prevent content duplication + await this.ensureCheckpointInitialization() let nextUserContent = userContent let includeFileDetails = true @@ -1142,6 +1149,60 @@ export class Task extends EventEmitter { } } + /** + * Ensures checkpoint initialization completes before proceeding with task execution. + * This prevents content duplication issues where checkpoint_saved messages arrive + * after model streaming has already started. + * + * @param timeout - Configurable timeout in milliseconds (default: 10 seconds) + */ + private async ensureCheckpointInitialization(): Promise { + if (!this.enableCheckpoints) { + return + } + + const provider = this.providerRef.deref() + if (!provider) { + return + } + + try { + // Start checkpoint service initialization + const service = getCheckpointService(this) + if (!service) { + return + } + + // Wait for initialization to complete or timeout + await pWaitFor( + () => { + if (this.abort) { + return true // Exit early if task is aborted + } + return service.isInitialized || !this.enableCheckpoints + }, + { + interval: 100, + timeout: this.checkpointInitTimeoutMs, + }, + ) + + // If still initializing after timeout, disable checkpoints and continue + if (!service.isInitialized && this.enableCheckpoints) { + provider.log( + `[Task#ensureCheckpointInitialization] checkpoint initialization timed out after ${this.checkpointInitTimeoutMs}ms, disabling checkpoints`, + ) + this.enableCheckpoints = false + } + } catch (error) { + // If checkpoint initialization fails, disable checkpoints and continue + provider?.log( + `[Task#ensureCheckpointInitialization] checkpoint initialization failed: ${error}, disabling checkpoints`, + ) + this.enableCheckpoints = false + } + } + public async recursivelyMakeClineRequests( userContent: Anthropic.Messages.ContentBlockParam[], includeFileDetails: boolean = false, From b76c6b43517113d21d2ba3ec234648474f1b3cf0 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Sat, 21 Jun 2025 18:46:47 -0500 Subject: [PATCH 2/2] test: add checkpoint initialization timeout tests for default and custom settings --- src/core/task/__tests__/Task.spec.ts | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 693f72d1c7..2cbc95de3a 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1334,5 +1334,52 @@ describe("Cline", () => { expect(task.diffStrategy).toBeUndefined() }) }) + + describe("checkpoint initialization timeout", () => { + it("should use default timeout when not specified", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + enableCheckpoints: true, + task: "test task", + startTask: false, + }) + + // Verify default timeout is set + expect(task.checkpointInitTimeoutMs).toBe(10000) // DEFAULT_CKPT_INIT_TIMEOUT_MS + }) + + it("should use custom timeout when specified", async () => { + const customTimeout = 5000 + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + enableCheckpoints: true, + checkpointInitTimeoutMs: customTimeout, + task: "test task", + startTask: false, + }) + + // Verify custom timeout is set + expect(task.checkpointInitTimeoutMs).toBe(customTimeout) + }) + + it("should disable checkpoints when not enabled", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + enableCheckpoints: false, + task: "test task", + startTask: false, + }) + + // Access private method through prototype + const ensureCheckpointInitialization = (task as any).ensureCheckpointInitialization.bind(task) + await ensureCheckpointInitialization() + + // Should remain disabled + expect(task.enableCheckpoints).toBe(false) + }) + }) }) })