Skip to content

Commit 4fb86d7

Browse files
committed
Add a repo-per-workspace checkpoints services in addition to repo-per-task
1 parent 3168b1e commit 4fb86d7

File tree

8 files changed

+308
-202
lines changed

8 files changed

+308
-202
lines changed

src/core/Cline.ts

Lines changed: 85 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import cloneDeep from "clone-deep"
3-
import { DiffStrategy, getDiffStrategy, UnifiedDiffStrategy } from "./diff/DiffStrategy"
3+
import { DiffStrategy, getDiffStrategy } from "./diff/DiffStrategy"
44
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
55
import delay from "delay"
66
import fs from "fs/promises"
@@ -13,7 +13,11 @@ import * as vscode from "vscode"
1313
import { ApiHandler, buildApiHandler } from "../api"
1414
import { ApiStream } from "../api/transform/stream"
1515
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
16-
import { ShadowCheckpointService } from "../services/checkpoints/ShadowCheckpointService"
16+
import {
17+
CheckpointServiceOptions,
18+
RepoPerTaskCheckpointService,
19+
RepoPerWorkspaceCheckpointService,
20+
} from "../services/checkpoints"
1721
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
1822
import {
1923
extractTextFromFile,
@@ -77,6 +81,7 @@ export type ClineOptions = {
7781
customInstructions?: string
7882
enableDiff?: boolean
7983
enableCheckpoints?: boolean
84+
checkpointStorage?: "task" | "workspace"
8085
fuzzyMatchThreshold?: number
8186
task?: string
8287
images?: string[]
@@ -115,8 +120,9 @@ export class Cline {
115120
isInitialized = false
116121

117122
// checkpoints
118-
enableCheckpoints: boolean = false
119-
private checkpointService?: ShadowCheckpointService
123+
private enableCheckpoints: boolean
124+
private checkpointStorage: "task" | "workspace"
125+
private checkpointService?: RepoPerTaskCheckpointService | RepoPerWorkspaceCheckpointService
120126

121127
// streaming
122128
isWaitingForFirstChunk = false
@@ -136,7 +142,8 @@ export class Cline {
136142
apiConfiguration,
137143
customInstructions,
138144
enableDiff,
139-
enableCheckpoints,
145+
enableCheckpoints = false,
146+
checkpointStorage = "workspace",
140147
fuzzyMatchThreshold,
141148
task,
142149
images,
@@ -160,7 +167,8 @@ export class Cline {
160167
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
161168
this.providerRef = new WeakRef(provider)
162169
this.diffViewProvider = new DiffViewProvider(cwd)
163-
this.enableCheckpoints = enableCheckpoints ?? false
170+
this.enableCheckpoints = enableCheckpoints
171+
this.checkpointStorage = checkpointStorage
164172

165173
// Initialize diffStrategy based on current state
166174
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
@@ -747,7 +755,8 @@ export class Cline {
747755
}
748756

749757
private async initiateTaskLoop(userContent: UserContent): Promise<void> {
750-
this.initializeCheckpoints()
758+
// Kicks off the checkpoints initialization process in the background.
759+
this.getCheckpointService()
751760

752761
let nextUserContent = userContent
753762
let includeFileDetails = true
@@ -3352,9 +3361,13 @@ export class Cline {
33523361

33533362
// Checkpoints
33543363

3355-
private async initializeCheckpoints() {
3364+
private getCheckpointService() {
33563365
if (!this.enableCheckpoints) {
3357-
return
3366+
return undefined
3367+
}
3368+
3369+
if (this.checkpointService) {
3370+
return this.checkpointService
33583371
}
33593372

33603373
const log = (message: string) => {
@@ -3368,45 +3381,36 @@ export class Cline {
33683381
}
33693382

33703383
try {
3371-
if (this.checkpointService) {
3372-
log("[Cline#initializeCheckpoints] checkpointService already initialized")
3373-
return
3374-
}
3375-
33763384
const workspaceDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
33773385

33783386
if (!workspaceDir) {
33793387
log("[Cline#initializeCheckpoints] workspace folder not found, disabling checkpoints")
33803388
this.enableCheckpoints = false
3381-
return
3389+
return undefined
33823390
}
33833391

3384-
const shadowDir = this.providerRef.deref()?.context.globalStorageUri.fsPath
3392+
const globalStorageDir = this.providerRef.deref()?.context.globalStorageUri.fsPath
33853393

3386-
if (!shadowDir) {
3387-
log("[Cline#initializeCheckpoints] shadowDir not found, disabling checkpoints")
3394+
if (!globalStorageDir) {
3395+
log("[Cline#initializeCheckpoints] globalStorageDir not found, disabling checkpoints")
33883396
this.enableCheckpoints = false
3389-
return
3397+
return undefined
33903398
}
33913399

3392-
const service = await ShadowCheckpointService.create({ taskId: this.taskId, workspaceDir, shadowDir, log })
3393-
3394-
if (!service) {
3395-
log("[Cline#initializeCheckpoints] failed to create checkpoint service, disabling checkpoints")
3396-
this.enableCheckpoints = false
3397-
return
3400+
const options: CheckpointServiceOptions = {
3401+
taskId: this.taskId,
3402+
workspaceDir,
3403+
shadowDir: globalStorageDir,
3404+
log,
33983405
}
33993406

3400-
service.on("initialize", ({ workspaceDir, created, duration }) => {
3401-
try {
3402-
if (created) {
3403-
log(`[Cline#initializeCheckpoints] created new shadow repo (${workspaceDir}) in ${duration}ms`)
3404-
} else {
3405-
log(
3406-
`[Cline#initializeCheckpoints] found existing shadow repo (${workspaceDir}) in ${duration}ms`,
3407-
)
3408-
}
3407+
const service =
3408+
this.checkpointStorage === "task"
3409+
? RepoPerTaskCheckpointService.create(options)
3410+
: RepoPerWorkspaceCheckpointService.create(options)
34093411

3412+
service.on("initialize", () => {
3413+
try {
34103414
this.checkpointService = service
34113415
this.checkpointSave()
34123416
} catch (err) {
@@ -3417,7 +3421,6 @@ export class Cline {
34173421

34183422
service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => {
34193423
try {
3420-
log(`[Cline#initializeCheckpoints] ${isFirst ? "initial" : "incremental"} checkpoint saved: ${to}`)
34213424
this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
34223425

34233426
this.say("checkpoint_saved", to, undefined, undefined, { isFirst, from, to }).catch((e) =>
@@ -3430,9 +3433,35 @@ export class Cline {
34303433
})
34313434

34323435
service.initShadowGit()
3436+
return service
34333437
} catch (err) {
3434-
log("[Cline#initializeCheckpoints] caught error in initializeCheckpoints(), disabling checkpoints")
3438+
log("[Cline#initializeCheckpoints] caught unexpected error, disabling checkpoints")
34353439
this.enableCheckpoints = false
3440+
return undefined
3441+
}
3442+
}
3443+
3444+
private async getInitializedCheckpointService({
3445+
interval = 250,
3446+
timeout = 15_000,
3447+
}: { interval?: number; timeout?: number } = {}) {
3448+
const service = this.getCheckpointService()
3449+
3450+
if (!service || service.isInitialized) {
3451+
return service
3452+
}
3453+
3454+
try {
3455+
await pWaitFor(
3456+
() => {
3457+
console.log("[Cline#getCheckpointService] waiting for service to initialize")
3458+
return service.isInitialized
3459+
},
3460+
{ interval, timeout },
3461+
)
3462+
return service
3463+
} catch (err) {
3464+
return undefined
34363465
}
34373466
}
34383467

@@ -3445,7 +3474,9 @@ export class Cline {
34453474
commitHash: string
34463475
mode: "full" | "checkpoint"
34473476
}) {
3448-
if (!this.checkpointService || !this.enableCheckpoints) {
3477+
const service = await this.getInitializedCheckpointService()
3478+
3479+
if (!service) {
34493480
return
34503481
}
34513482

@@ -3461,7 +3492,7 @@ export class Cline {
34613492
}
34623493

34633494
try {
3464-
const changes = await this.checkpointService.getDiff({ from: previousCommitHash, to: commitHash })
3495+
const changes = await service.getDiff({ from: previousCommitHash, to: commitHash })
34653496

34663497
if (!changes?.length) {
34673498
vscode.window.showInformationMessage("No changes found.")
@@ -3488,12 +3519,22 @@ export class Cline {
34883519
}
34893520

34903521
public checkpointSave() {
3491-
if (!this.checkpointService || !this.enableCheckpoints) {
3522+
const service = this.getCheckpointService()
3523+
3524+
if (!service) {
3525+
return
3526+
}
3527+
3528+
if (!service.isInitialized) {
3529+
this.providerRef
3530+
.deref()
3531+
?.log("[checkpointSave] checkpoints didn't initialize in time, disabling checkpoints for this task")
3532+
this.enableCheckpoints = false
34923533
return
34933534
}
34943535

34953536
// Start the checkpoint process in the background.
3496-
this.checkpointService.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`)
3537+
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`)
34973538
}
34983539

34993540
public async checkpointRestore({
@@ -3505,7 +3546,9 @@ export class Cline {
35053546
commitHash: string
35063547
mode: "preview" | "restore"
35073548
}) {
3508-
if (!this.checkpointService || !this.enableCheckpoints) {
3549+
const service = await this.getInitializedCheckpointService()
3550+
3551+
if (!service) {
35093552
return
35103553
}
35113554

@@ -3516,7 +3559,7 @@ export class Cline {
35163559
}
35173560

35183561
try {
3519-
await this.checkpointService.restoreCheckpoint(commitHash)
3562+
await service.restoreCheckpoint(commitHash)
35203563

35213564
await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
35223565

src/core/webview/ClineProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
887887

888888
if (result.success) {
889889
await this.cline?.checkpointDiff(result.data)
890+
} else {
891+
vscode.window.showErrorMessage("Invalid checkpoint diff payload.")
890892
}
891893

892894
break
@@ -907,6 +909,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
907909
} catch (error) {
908910
vscode.window.showErrorMessage("Failed to restore checkpoint.")
909911
}
912+
} else {
913+
vscode.window.showErrorMessage("Invalid checkpoint restore payload.")
910914
}
911915

912916
break
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as path from "path"
2+
3+
import { CheckpointServiceOptions } from "./types"
4+
import { ShadowCheckpointService } from "./ShadowCheckpointService"
5+
6+
export class RepoPerTaskCheckpointService extends ShadowCheckpointService {
7+
public static create({ taskId, workspaceDir, shadowDir, log = console.log }: CheckpointServiceOptions) {
8+
return new RepoPerTaskCheckpointService(
9+
taskId,
10+
path.join(shadowDir, "tasks", taskId, "checkpoints"),
11+
workspaceDir,
12+
log,
13+
)
14+
}
15+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as path from "path"
2+
import crypto from "crypto"
3+
4+
import { CheckpointServiceOptions } from "./types"
5+
import { ShadowCheckpointService } from "./ShadowCheckpointService"
6+
7+
export class RepoPerWorkspaceCheckpointService extends ShadowCheckpointService {
8+
private async checkoutTaskBranch() {
9+
if (!this.git) {
10+
throw new Error("Shadow git repo not initialized")
11+
}
12+
13+
const startTime = Date.now()
14+
15+
const branch = `roo-${this.taskId}`
16+
this.log(`[${this.constructor.name}#checkoutTaskBranch] searching branches for ${branch}`)
17+
const branches = await this.git.branchLocal()
18+
19+
if (!branches.all.includes(branch)) {
20+
this.log(`[${this.constructor.name}#checkoutTaskBranch] creating ${branch}`)
21+
await this.git.checkoutLocalBranch(branch)
22+
} else {
23+
this.log(`[${this.constructor.name}#checkoutTaskBranch] checking out ${branch}`)
24+
await this.git.checkout(branch)
25+
}
26+
27+
const result = await this.git.revparse(["--abbrev-ref", "HEAD"])
28+
29+
const duration = Date.now() - startTime
30+
this.log(`[${this.constructor.name}#checkoutTaskBranch] checked out ${result} in ${duration}ms`)
31+
}
32+
33+
override async initShadowGit() {
34+
await super.initShadowGit()
35+
await this.checkoutTaskBranch()
36+
}
37+
38+
override async restoreCheckpoint(commitHash: string) {
39+
await this.checkoutTaskBranch()
40+
await super.restoreCheckpoint(commitHash)
41+
}
42+
43+
public static create({ taskId, workspaceDir, shadowDir, log = console.log }: CheckpointServiceOptions) {
44+
const workspaceHash = crypto.createHash("sha256").update(workspaceDir).digest("hex").toString()
45+
46+
return new RepoPerWorkspaceCheckpointService(
47+
taskId,
48+
path.join(shadowDir, "checkpoints", workspaceHash),
49+
workspaceDir,
50+
log,
51+
)
52+
}
53+
}

0 commit comments

Comments
 (0)