feat(FN-4508): complete Step 2 — handle tip-already-merged recovery paths
Fusion-Task-Id: FN-4508 Fusion-Task-Lineage: 948a9cae-3975-4f15-bd47-2f88b379171d
This commit is contained in:
@@ -7141,12 +7141,42 @@ Backward compat fallback: if JSON is unavailable, you may still begin output wit
|
||||
});
|
||||
|
||||
if (inspection.kind === "stale-resolved") {
|
||||
await this.store.updateTask(task.id, { worktree: null, branch: null, baseCommitSha: null });
|
||||
const message = `[recovery] ${task.id} stage-A: pruned stale admin entry for ${error.branchName}`;
|
||||
await this.store.logEntry(task.id, message, undefined, this.currentRunContext);
|
||||
await this.store.appendAgentLog(task.id, "Branch conflict auto-recovery", "text", message, "executor");
|
||||
return "retry";
|
||||
}
|
||||
|
||||
if (inspection.kind === "tip-already-merged") {
|
||||
if (inspection.livePath) {
|
||||
await this.cleanupConflictingWorktree(inspection.livePath, error.branchName, task.id);
|
||||
}
|
||||
try {
|
||||
await execAsync("git worktree prune", {
|
||||
cwd: this.rootDir,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
try {
|
||||
await execAsync(`git branch -D ${JSON.stringify(error.branchName)}`, {
|
||||
cwd: this.rootDir,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
await this.store.updateTask(task.id, { worktree: null, branch: null, baseCommitSha: null });
|
||||
const message = `[recovery] ${task.id} stage-A: tip-already-merged cleanup for ${error.branchName} (${inspection.tipSha.slice(0, 12)} on ${inspection.integrationRef})`;
|
||||
await this.store.logEntry(task.id, message, undefined, this.currentRunContext);
|
||||
await this.store.appendAgentLog(task.id, "Branch conflict auto-recovery", "text", message, "executor");
|
||||
return "retry";
|
||||
}
|
||||
|
||||
if (inspection.kind === "reclaimable") {
|
||||
await this.reclaimExistingWorktree(task, inspection.livePath, error.branchName, inspection.tipSha, inspection.taskAttributedCommitCount);
|
||||
return "reclaimed";
|
||||
@@ -7830,7 +7860,7 @@ Backward compat fallback: if JSON is unavailable, you may still begin output wit
|
||||
startPoint,
|
||||
});
|
||||
|
||||
if (inspection.kind === "stale" || inspection.kind === "stale-resolved") {
|
||||
if (inspection.kind === "stale" || inspection.kind === "stale-resolved" || inspection.kind === "tip-already-merged") {
|
||||
const cleanupSuccess = await this.cleanupConflictingWorktree(conflictPath, branch, taskId);
|
||||
if (cleanupSuccess) {
|
||||
await this.store.logEntry(taskId, `Cleaned up conflicting worktree, retrying`, path);
|
||||
|
||||
@@ -1484,6 +1484,87 @@ export class SelfHealingManager {
|
||||
if (inspection.kind === "stale" || inspection.kind === "stale-resolved") {
|
||||
continue;
|
||||
}
|
||||
if (inspection.kind === "tip-already-merged") {
|
||||
const branchName = task.branch;
|
||||
let reclaimedCleanly = false;
|
||||
try {
|
||||
if (inspection.livePath && existsSync(inspection.livePath)) {
|
||||
await execAsync(`git worktree remove --force ${JSON.stringify(inspection.livePath)}`, {
|
||||
cwd: this.options.rootDir,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
await execAsync("git worktree prune", {
|
||||
cwd: this.options.rootDir,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
await execAsync(`git branch -D ${JSON.stringify(branchName)}`, {
|
||||
cwd: this.options.rootDir,
|
||||
timeout: 120_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
await this.store.updateTask(task.id, {
|
||||
worktree: null,
|
||||
branch: null,
|
||||
baseCommitSha: null,
|
||||
paused: false,
|
||||
pausedReason: undefined,
|
||||
status: null,
|
||||
error: null,
|
||||
});
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`[recovery] tip-already-merged ${task.id} branch=${branchName} tip=${inspection.tipSha.slice(0, 12)} integrationRef=${inspection.integrationRef} reason=stale-cached-metadata-ghost-conflict`,
|
||||
);
|
||||
|
||||
if (task.column === "in-review") {
|
||||
await this.store.moveTask(task.id, "todo", {
|
||||
moveSource: "engine",
|
||||
preserveProgress: true,
|
||||
preserveResumeState: true,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const auditor = createRunAuditor(this.store, {
|
||||
runId: generateSyntheticRunId("self-heal", task.id),
|
||||
agentId: "self-healing",
|
||||
taskId: task.id,
|
||||
taskLineageId: task.lineageId,
|
||||
phase: "tip-already-merged",
|
||||
});
|
||||
await auditor.git({
|
||||
type: "branch:auto-reclaim",
|
||||
target: branchName,
|
||||
metadata: {
|
||||
taskId: task.id,
|
||||
branch: branchName,
|
||||
worktreePath: inspection.livePath,
|
||||
existingTipSha: inspection.tipSha,
|
||||
integrationRef: inspection.integrationRef,
|
||||
trigger: "self-healing-sweep-ghost-conflict",
|
||||
},
|
||||
});
|
||||
} catch (auditErr: unknown) {
|
||||
log.warn(`Failed to write tip-already-merged run-audit event for ${task.id}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`);
|
||||
}
|
||||
|
||||
recovered++;
|
||||
reclaimedCleanly = true;
|
||||
} catch (tipMergedErr: unknown) {
|
||||
const message = tipMergedErr instanceof Error ? tipMergedErr.message : String(tipMergedErr);
|
||||
await this.store.logEntry(task.id, `Auto-recovery warning: tip-already-merged cleanup failed — ${message}`);
|
||||
log.warn(`Failed tip-already-merged cleanup for ${task.id}: ${message}`);
|
||||
}
|
||||
|
||||
if (reclaimedCleanly) {
|
||||
continue;
|
||||
}
|
||||
throw new Error(`tip-already-merged cleanup failed for ${task.id}`);
|
||||
}
|
||||
if (inspection.kind === "live-foreign") {
|
||||
throw inspection.error;
|
||||
}
|
||||
|
||||
@@ -267,8 +267,15 @@ export class WorktreePool {
|
||||
ownerTaskId: taskId,
|
||||
startPoint: base,
|
||||
});
|
||||
if (inspection.kind === "stale" || inspection.kind === "stale-resolved") {
|
||||
if (inspection.kind === "stale" || inspection.kind === "stale-resolved" || inspection.kind === "tip-already-merged") {
|
||||
await execAsync("git worktree prune", { cwd: worktreePath });
|
||||
if (inspection.kind === "tip-already-merged") {
|
||||
try {
|
||||
await execAsync(`git branch -D "${branchName}"`, { cwd: worktreePath });
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
await execAsync(checkoutCmd, { cwd: worktreePath });
|
||||
await assertCleanBranchAtBase(worktreePath, branchName, resolvedBase, taskId);
|
||||
return { branch: branchName, worktreePath, reclaimed: false };
|
||||
|
||||
Reference in New Issue
Block a user