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
19 changes: 11 additions & 8 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../se

export async function getCheckpointService(
task: Task,
{ interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
{
interval = 250,
timeout = 15_000,
isInitialCall = false,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new isInitialCall parameter lacks JSDoc documentation. Could we add documentation explaining its purpose?

}: { interval?: number; timeout?: number; isInitialCall?: boolean } = {},
) {
if (!task.enableCheckpoints) {
return undefined
Expand Down Expand Up @@ -67,13 +71,12 @@ export async function getCheckpointService(
}

if (task.checkpointServiceInitializing) {
await pWaitFor(
() => {
console.log("[Task#getCheckpointService] waiting for service to initialize")
return !!task.checkpointService && !!task?.checkpointService?.isInitialized
},
{ interval, timeout },
)
// For initial calls, don't apply timeout to allow large repositories to initialize
const waitOptions = isInitialCall ? { interval } : { interval, timeout }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right approach? The issue discussion indicates that the problem is subsequent calls timing out while waiting for initialization, not the initial call itself. This implementation removes the timeout for initial calls, but the actual bottleneck (git add during initialization) still has a hard-coded 5-second timeout in ShadowCheckpointService. Could we consider a more comprehensive solution?

await pWaitFor(() => {
console.log("[Task#getCheckpointService] waiting for service to initialize")
return !!task.checkpointService && !!task?.checkpointService?.isInitialized
}, waitOptions)
if (!task?.checkpointService) {
task.enableCheckpoints = false
return undefined
Expand Down
3 changes: 2 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1661,7 +1661,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise<void> {
// Kicks off the checkpoints initialization process in the background.
getCheckpointService(this)
// Pass isInitialCall=true to allow unlimited time for large repositories
getCheckpointService(this, { isInitialCall: true })

let nextUserContent = userContent
let includeFileDetails = true
Expand Down
21 changes: 19 additions & 2 deletions src/services/checkpoints/ShadowCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,21 @@ export abstract class ShadowCheckpointService extends EventEmitter {
await git.addConfig("user.name", "Roo Code")
await git.addConfig("user.email", "[email protected]")
await this.writeExcludeFile()
await this.stageAll(git)

// For initial commit, stage files but with a timeout to prevent hanging on large repos
// Use a promise race to ensure we don't wait forever
const stagePromise = this.stageAll(git)
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
this.log(
`[${this.constructor.name}#initShadowGit] Initial staging timed out after 5 seconds, proceeding with empty commit`,
)
resolve()
}, 5000) // 5 second timeout for initial staging
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition of the --ignore-errors flag! However, the 5-second timeout is a magic number. Consider defining it as a constant at the top of the file:

This would make it easier to adjust if needed.

})

await Promise.race([stagePromise, timeoutPromise])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Promise.race() implementation could lead to incomplete staging. If the timeout wins, the staging operation continues in the background but we proceed with a potentially empty commit. This could miss important files. Consider:

  1. Cancelling the staging operation if timeout occurs
  2. Or waiting for staging to complete but with a longer, configurable timeout
  3. Or deferring staging until the first actual checkpoint (lazy staging)


const { commit } = await git.commit("initial commit", { "--allow-empty": null })
this.baseHash = commit
created = true
Expand Down Expand Up @@ -145,11 +159,14 @@ export abstract class ShadowCheckpointService extends EventEmitter {

private async stageAll(git: SimpleGit) {
try {
await git.add(".")
// Add --ignore-errors flag to handle permission issues gracefully
// This prevents the operation from failing on files with permission issues
await git.add([".", "--ignore-errors"])
} catch (error) {
this.log(
`[${this.constructor.name}#stageAll] failed to add files to git: ${error instanceof Error ? error.message : String(error)}`,
)
// Don't throw - allow the operation to continue with whatever files were successfully staged
}
}

Expand Down
Loading