fix(FN-1249): degrade failed wave worktrees to sequential fallback

- Track per-step worktree creation failures in step-session execution waves
- Split wave execution into parallel and sequential phases so failed-worktree steps run on the primary worktree
- Cherry-pick only successful parallel-step commits and return consistently ordered results
- Add comprehensive tests covering partial/all worktree creation failures, fallback ordering, and cleanup behavior
This commit is contained in:
gsxdsm
2026-04-08 09:47:26 -07:00
parent 6a0e3a4c5b
commit 478b2eec4c
2 changed files with 435 additions and 24 deletions

View File

@@ -774,41 +774,71 @@ export class StepSessionExecutor {
// Create worktrees for each step in the wave
const worktreePaths = new Map<number, string>();
const failedWorktreeSteps = new Set<number>();
for (const stepIdx of wave.indices) {
try {
const path = await this.createStepWorktree(stepIdx);
worktreePaths.set(stepIdx, path);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
stepExecLog.error(`Failed to create worktree for step ${stepIdx}: ${errorMessage}`);
// If worktree creation fails, this step fails immediately
worktreePaths.set(stepIdx, this.options.worktreePath);
stepExecLog.error(
`Failed to create worktree for step ${stepIdx}: ${errorMessage}. ` +
`Step will be deferred to sequential fallback on primary worktree.`,
);
failedWorktreeSteps.add(stepIdx);
}
}
// Execute all steps concurrently
const promises = wave.indices.map((stepIdx) => {
const path = worktreePaths.get(stepIdx) ?? this.options.worktreePath;
return this.executeStep(stepIdx, path);
});
const sequentialFallbackSteps = [...failedWorktreeSteps].sort((a, b) => a - b);
const parallelSteps = wave.indices.filter((stepIdx) => !failedWorktreeSteps.has(stepIdx));
const settled = await Promise.allSettled(promises);
const results: StepResult[] = settled.map((outcome, i) => {
if (outcome.status === "fulfilled") {
return outcome.value;
}
// Promise rejected (unexpected — should be caught inside executeStep)
const stepIdx = wave.indices[i]!;
return {
stepIndex: stepIdx,
success: false,
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
retries: 0,
};
});
if (sequentialFallbackSteps.length > 0) {
stepExecLog.warn(
`Wave ${wave.waveNumber}: worktree creation failed for step(s) ` +
`[${sequentialFallbackSteps.join(", ")}]; degrading those step(s) to sequential ` +
`execution on primary worktree for task ${taskDetail.id}`,
);
}
// Phase 1: Execute successful-worktree steps in parallel.
const parallelResults: StepResult[] = [];
if (parallelSteps.length > 0) {
const promises = parallelSteps.map((stepIdx) => {
const path = worktreePaths.get(stepIdx) ?? this.options.worktreePath;
return this.executeStep(stepIdx, path);
});
const settled = await Promise.allSettled(promises);
parallelResults.push(...settled.map((outcome, i) => {
if (outcome.status === "fulfilled") {
return outcome.value;
}
// Promise rejected (unexpected — should be caught inside executeStep)
const stepIdx = parallelSteps[i]!;
return {
stepIndex: stepIdx,
success: false,
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
retries: 0,
};
}));
}
// Phase 2: Execute failed-worktree steps sequentially on primary worktree.
const sequentialResults: StepResult[] = [];
for (const stepIdx of sequentialFallbackSteps) {
stepExecLog.warn(
`Wave ${wave.waveNumber}: executing step ${stepIdx} sequentially on primary worktree ` +
`after worktree creation failure for task ${taskDetail.id}`,
);
const result = await this.executeStep(stepIdx, this.options.worktreePath);
sequentialResults.push(result);
}
const results = [...parallelResults, ...sequentialResults];
// Cherry-pick successful steps into primary worktree
for (const stepIdx of wave.indices) {
for (const stepIdx of parallelSteps) {
const result = results.find((r) => r.stepIndex === stepIdx);
const worktreePath = worktreePaths.get(stepIdx);
@@ -851,7 +881,7 @@ export class StepSessionExecutor {
}
}
return results;
return results.sort((a, b) => a.stepIndex - b.stepIndex);
}
// ── Internal: Worktree Management ───────────────────────────────────