fix: self-healing stale worktree recovery in SimpleGitWorktreeManager
When git worktree add fails with "branch already used by worktree at <path>", parse the stale path, force-remove it, and retry once. Fixes blocked task retries where the old agent-workdirs directory still exists on disk and git worktree prune alone can't clear the reference.
This commit is contained in:
@@ -85,10 +85,32 @@ export class SimpleGitWorktreeManager implements WorktreeManager {
|
||||
}
|
||||
// Prune stale worktree references before adding new one
|
||||
await this.git.raw(['worktree', 'prune']);
|
||||
await this.git.raw(['worktree', 'add', worktreePath, branch]);
|
||||
try {
|
||||
await this.git.raw(['worktree', 'add', worktreePath, branch]);
|
||||
} catch (err) {
|
||||
const stalePath = this.parseStaleWorktreePath(err);
|
||||
if (stalePath) {
|
||||
log.warn({ branch, stalePath }, 'branch locked to stale worktree, force-removing and retrying');
|
||||
await this.forceRemoveWorktree(stalePath);
|
||||
await this.git.raw(['worktree', 'add', worktreePath, branch]);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// git worktree add -b <branch> <path> <base-branch>
|
||||
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
|
||||
try {
|
||||
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
|
||||
} catch (err) {
|
||||
const stalePath = this.parseStaleWorktreePath(err);
|
||||
if (stalePath) {
|
||||
log.warn({ branch, stalePath }, 'branch locked to stale worktree, force-removing and retrying');
|
||||
await this.forceRemoveWorktree(stalePath);
|
||||
await this.git.raw(['worktree', 'add', '-b', branch, worktreePath, baseBranch]);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const worktree: Worktree = {
|
||||
@@ -300,6 +322,25 @@ export class SimpleGitWorktreeManager implements WorktreeManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse stale worktree path from "already used by worktree at '<path>'" git error. */
|
||||
private parseStaleWorktreePath(err: unknown): string | null {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const match = msg.match(/already used by worktree at '([^']+)'/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/** Force-remove a stale worktree (git ref + directory). */
|
||||
private async forceRemoveWorktree(worktreePath: string): Promise<void> {
|
||||
try {
|
||||
await this.git.raw(['worktree', 'remove', '--force', worktreePath]);
|
||||
} catch {
|
||||
// Corrupted state fallback: delete directory and prune
|
||||
const { rm } = await import('node:fs/promises');
|
||||
await rm(worktreePath, { recursive: true, force: true });
|
||||
await this.git.raw(['worktree', 'prune']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the porcelain output of git worktree list.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user