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:
Fusion
2026-05-14 16:02:21 -07:00
committed by gsxdsm
parent 663e5d2cdc
commit 473ea373b8
3 changed files with 120 additions and 2 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 };