Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand All @@ -105,6 +108,7 @@ export type TaskOptions = {
apiConfiguration: ProviderSettings
enableDiff?: boolean
enableCheckpoints?: boolean
checkpointInitTimeoutMs?: number
fuzzyMatchThreshold?: number
consecutiveMistakeLimit?: number
task?: string
Expand Down Expand Up @@ -186,6 +190,7 @@ export class Task extends EventEmitter<ClineEvents> {

// Checkpoints
enableCheckpoints: boolean
checkpointInitTimeoutMs: number
checkpointService?: RepoPerTaskCheckpointService
checkpointServiceInitializing = false

Expand All @@ -207,6 +212,7 @@ export class Task extends EventEmitter<ClineEvents> {
apiConfiguration,
enableDiff = false,
enableCheckpoints = true,
checkpointInitTimeoutMs = DEFAULT_CKPT_INIT_TIMEOUT_MS,
fuzzyMatchThreshold = 1.0,
consecutiveMistakeLimit = 3,
task,
Expand Down Expand Up @@ -252,6 +258,7 @@ export class Task extends EventEmitter<ClineEvents> {
this.globalStoragePath = provider.context.globalStorageUri.fsPath
this.diffViewProvider = new DiffViewProvider(this.cwd)
this.enableCheckpoints = enableCheckpoints
this.checkpointInitTimeoutMs = checkpointInitTimeoutMs

this.rootTask = rootTask
this.parentTask = parentTask
Expand Down Expand Up @@ -1108,8 +1115,8 @@ export class Task extends EventEmitter<ClineEvents> {
// Task Loop

private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise<void> {
// 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
Expand Down Expand Up @@ -1142,6 +1149,60 @@ export class Task extends EventEmitter<ClineEvents> {
}
}

/**
* 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<void> {
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,
Expand Down
47 changes: 47 additions & 0 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
})