fix(engine): surface phantom-finalize cause and reset HEAD when branch is authoritative
commitOrAmendMergeWithFixes used to swallow every unexpected throw as `reason: "unknown-phantom"` and the two callers re-threw a bare "verification fix finalize failed (unknown phantom)" with no operator- actionable signal. FN-5422-class wedges (preAttemptHeadSha == currentHead but branchTip ahead with task-trailered commits) couldn't be diagnosed without re-running. The catch now records the original error and probes whether the branch ref itself is authoritative for the task (tip carries Fusion-Task-Id trailer, base..branch is foreign-contamination-free). When it is — i.e. the work isn't lost, the integration worktree just didn't advance — the catch resets rootDir to preAttemptHeadSha so the next merge attempt starts from a known baseline instead of inheriting partial squash state, and returns `reason: "branch-ref-ahead-reset"`. The two callers fold `originalError=` and `branchAuthority=` into the thrown message so the real cause (diff-volume gate, file-scope, transient git, etc.) is visible in the run log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
.changeset/fix-phantom-finalize-diagnostic-reset.md
Normal file
9
.changeset/fix-phantom-finalize-diagnostic-reset.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Engine reliability: better diagnostics + clean-baseline reset on phantom finalize.
|
||||||
|
|
||||||
|
- `commitOrAmendMergeWithFixes` previously swallowed all unexpected errors as `reason: "unknown-phantom"` and the two callers re-threw a `verification fix finalize failed (unknown phantom)` error with no surface area beyond the SHAs. FN-5422-class tasks wedged in review with no actionable signal in the failure message.
|
||||||
|
- The catch now captures the original error message and runs an `isBranchAuthoritativeForTask` probe (existing branch ref carries this task's `Fusion-Task-Id` trailer + foreign-contamination check against base). When the branch ref is authoritative — meaning the AI's work is safely stored on `fusion/<id>` and only the in-merge attempt's integration worktree drifted — the catch resets rootDir to `preAttemptHeadSha` and returns `reason: "branch-ref-ahead-reset"`. The next merge attempt then starts from a known-good baseline instead of inheriting half-built squash state.
|
||||||
|
- Verification-fix and build-verification-fix callers now include the original error and the branch-authority probe outcome in the thrown error, so operators see the actual failure cause (e.g. diff-volume regression, file-scope violation, transient git error) rather than `unknown phantom`.
|
||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
filterFilesToOwnTaskCommits,
|
filterFilesToOwnTaskCommits,
|
||||||
SilentNoOpAttributionMismatchError,
|
SilentNoOpAttributionMismatchError,
|
||||||
} from "./branch-attribution.js";
|
} from "./branch-attribution.js";
|
||||||
|
import { isBranchAuthoritativeForTask } from "./branch-conflicts.js";
|
||||||
import { hostname } from "node:os";
|
import { hostname } from "node:os";
|
||||||
import {
|
import {
|
||||||
buildTaskLineageTrailer,
|
buildTaskLineageTrailer,
|
||||||
@@ -3369,7 +3370,7 @@ type MergeFinalizeResult =
|
|||||||
mergeSha?: string;
|
mergeSha?: string;
|
||||||
strategy?: AlreadyMergedDetectionStrategy;
|
strategy?: AlreadyMergedDetectionStrategy;
|
||||||
}
|
}
|
||||||
| { ok: false; reason: "fix-produced-no-content" | "unknown-phantom" };
|
| { ok: false; reason: "fix-produced-no-content" | "unknown-phantom" | "branch-ref-ahead-reset"; originalError?: string; branchAuthority?: "ok" | string };
|
||||||
|
|
||||||
async function persistFinalizeResetLeftovers(rootDir: string, taskId: string, store?: TaskStore): Promise<void> {
|
async function persistFinalizeResetLeftovers(rootDir: string, taskId: string, store?: TaskStore): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -3963,7 +3964,35 @@ export async function commitOrAmendMergeWithFixes(
|
|||||||
}
|
}
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
mergerLog.warn(`${taskId}: failed to finalize merge commit: ${errorMessage}`);
|
mergerLog.warn(`${taskId}: failed to finalize merge commit: ${errorMessage}`);
|
||||||
return { ok: false, reason: "unknown-phantom" };
|
// FN-5422-class diagnostic: when finalize throws but the branch ref still
|
||||||
|
// carries this task's authoritative lineage (tip trailer + no foreign
|
||||||
|
// FN-attributed commits in base..branch), the work isn't lost — it's just
|
||||||
|
// that this attempt's integration worktree never advanced. Reset rootDir
|
||||||
|
// to preAttemptHeadSha so the next merge attempt starts clean, and tag
|
||||||
|
// the result so the caller's diagnostic includes that context.
|
||||||
|
const authority = await isBranchAuthoritativeForTask(rootDir, branch, taskId, preAttemptHeadSha).catch(
|
||||||
|
() => ({ ok: false as const, reason: "authority-probe-failed" }),
|
||||||
|
);
|
||||||
|
if (authority.ok) {
|
||||||
|
try {
|
||||||
|
await execAsync(`git reset --hard ${preAttemptHeadSha}`, { cwd: rootDir, encoding: "utf-8" });
|
||||||
|
await execAsync("git clean -fd", { cwd: rootDir, encoding: "utf-8" });
|
||||||
|
mergerLog.warn(
|
||||||
|
`${taskId}: finalize threw but branch ref is authoritative (tip carries Fusion-Task-Id, no foreign commits since base) — reset HEAD to ${preAttemptHeadSha.slice(0, 8)} for clean retry`,
|
||||||
|
);
|
||||||
|
} catch (resetErr: unknown) {
|
||||||
|
mergerLog.warn(
|
||||||
|
`${taskId}: branch-authoritative reset failed: ${resetErr instanceof Error ? resetErr.message : String(resetErr)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { ok: false, reason: "branch-ref-ahead-reset", originalError: errorMessage, branchAuthority: "ok" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "unknown-phantom",
|
||||||
|
originalError: errorMessage,
|
||||||
|
branchAuthority: authority.ok ? "ok" : authority.reason,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8438,9 +8467,13 @@ export async function aiMergeTask(
|
|||||||
resetMergeWithWarn(rootDir, taskId, "verification-fix finalize");
|
resetMergeWithWarn(rootDir, taskId, "verification-fix finalize");
|
||||||
const classification = finalized.reason === "fix-produced-no-content"
|
const classification = finalized.reason === "fix-produced-no-content"
|
||||||
? "fix produced no content"
|
? "fix produced no content"
|
||||||
: "unknown phantom";
|
: finalized.reason === "branch-ref-ahead-reset"
|
||||||
|
? "branch-ref ahead of integration target (reset for retry)"
|
||||||
|
: "unknown phantom";
|
||||||
|
const originalErrorSuffix = finalized.originalError ? ` originalError="${finalized.originalError}";` : "";
|
||||||
|
const authoritySuffix = finalized.branchAuthority ? ` branchAuthority=${finalized.branchAuthority};` : "";
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${taskId}: verification fix finalize failed (${classification}); preAttemptHeadSha=${preAttemptHeadSha}; currentHead=${currentHeadOut.trim()}; branch=${branch}; branchTip=${branchTipOut.trim()}.`,
|
`${taskId}: verification fix finalize failed (${classification}); preAttemptHeadSha=${preAttemptHeadSha}; currentHead=${currentHeadOut.trim()}; branch=${branch}; branchTip=${branchTipOut.trim()};${originalErrorSuffix}${authoritySuffix}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true; // Merge succeeds
|
return true; // Merge succeeds
|
||||||
@@ -8582,9 +8615,13 @@ export async function aiMergeTask(
|
|||||||
resetMergeWithWarn(rootDir, taskId, "build-verification fix finalize");
|
resetMergeWithWarn(rootDir, taskId, "build-verification fix finalize");
|
||||||
const classification = finalized.reason === "fix-produced-no-content"
|
const classification = finalized.reason === "fix-produced-no-content"
|
||||||
? "fix produced no content"
|
? "fix produced no content"
|
||||||
: "unknown phantom";
|
: finalized.reason === "branch-ref-ahead-reset"
|
||||||
|
? "branch-ref ahead of integration target (reset for retry)"
|
||||||
|
: "unknown phantom";
|
||||||
|
const originalErrorSuffix = finalized.originalError ? ` originalError="${finalized.originalError}";` : "";
|
||||||
|
const authoritySuffix = finalized.branchAuthority ? ` branchAuthority=${finalized.branchAuthority};` : "";
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${taskId}: build verification fix finalize failed (${classification}); preAttemptHeadSha=${preAttemptHeadSha}; currentHead=${currentHeadOut.trim()}; branch=${branch}; branchTip=${branchTipOut.trim()}.`,
|
`${taskId}: build verification fix finalize failed (${classification}); preAttemptHeadSha=${preAttemptHeadSha}; currentHead=${currentHeadOut.trim()}; branch=${branch}; branchTip=${branchTipOut.trim()};${originalErrorSuffix}${authoritySuffix}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true; // Merge succeeds
|
return true; // Merge succeeds
|
||||||
|
|||||||
Reference in New Issue
Block a user