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
|
// Prune stale worktree references before adding new one
|
||||||
await this.git.raw(['worktree', 'prune']);
|
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 {
|
} else {
|
||||||
// git worktree add -b <branch> <path> <base-branch>
|
// 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 = {
|
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.
|
* Parse the porcelain output of git worktree list.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user