fix(FN-7248): skip completed steps on resume

This commit is contained in:
gsxdsm
2026-06-29 19:23:38 -07:00
parent ed21597690
commit be9e231004
2 changed files with 88 additions and 3 deletions

View File

@@ -1274,6 +1274,52 @@ describe("StepSessionExecutor", () => {
expect(onStepStart).toHaveBeenNthCalledWith(3, 2);
});
it("skips live-terminal steps before starting resumed sessions", async () => {
const prompt = makeStepPrompt("FN-7248", 2);
const task = makeTaskDetail({
id: "FN-7248",
prompt,
steps: [
{ name: "Preflight", status: "pending" },
{ name: "Implement", status: "pending" },
],
});
const settings = makeSettings({ maxParallelSteps: 1 });
const session = makeMockSession();
mockedCreateFnAgent.mockResolvedValue({ session } as any);
const onStepStart = vi.fn();
const store = {
getTask: vi.fn().mockResolvedValue({
...task,
steps: [
{ name: "Preflight", status: "done" },
{ name: "Implement", status: "in-progress" },
],
}),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
};
const executor = new StepSessionExecutor({
store: store as any,
taskDetail: task,
worktreePath: "/project/.worktrees/main",
rootDir: "/project",
settings,
onStepStart,
});
const results = await executor.executeAll();
/*
* FNXC:WorkflowResume 2026-06-29-18:26:
* Resume must use TaskStore as the authoritative projection before scheduling per-step sessions. A stale snapshot may still list Step 0 as pending, but if the live task says Step 0 is done then no Step 0 session or onStepStart callback may run.
*/
expect(results.map((result) => result.stepIndex)).toEqual([1]);
expect(onStepStart).toHaveBeenCalledTimes(1);
expect(onStepStart).toHaveBeenCalledWith(1);
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
});
it("includes token usage from session stats on successful step completion", async () => {
const prompt = makeStepPrompt("FN-001", 1);
const task = makeTaskDetail({

View File

@@ -791,14 +791,23 @@ export class StepSessionExecutor {
for (const wave of waves) {
if (this.aborted) break;
if (wave.indices.length === 1) {
/*
* FNXC:WorkflowResume 2026-06-29-18:26:
* Engine restart/resume may construct StepSessionExecutor with a stale taskDetail snapshot while TaskStore already shows earlier steps as done/skipped. Re-read live step status before scheduling each wave so resumed graph-owned execution does not fire onStepStart for completed steps and produce noisy done→in-progress regressions like FN-7248.
*/
const runnableIndices = await this.filterLiveRunnableStepIndices(wave.indices);
if (runnableIndices.length === 0) {
continue;
}
if (runnableIndices.length === 1) {
// Single step — use primary worktree
const stepIdx = wave.indices[0]!;
const stepIdx = runnableIndices[0]!;
const result = await this.executeStep(stepIdx, this.options.worktreePath);
this.stepResults.push(result);
} else {
// Multiple steps — parallel wave
const waveResults = await this.executeParallelWave(wave);
const waveResults = await this.executeParallelWave({ ...wave, indices: runnableIndices });
this.stepResults.push(...waveResults);
}
}
@@ -807,6 +816,36 @@ export class StepSessionExecutor {
return this.stepResults.sort((a, b) => a.stepIndex - b.stepIndex);
}
private async filterLiveRunnableStepIndices(stepIndices: number[]): Promise<number[]> {
if (!this.options.store || stepIndices.length === 0) {
return stepIndices;
}
try {
const liveTask = await this.options.store.getTask(this.options.taskDetail.id);
if (!liveTask || liveTask.id !== this.options.taskDetail.id) {
return stepIndices;
}
return stepIndices.filter((stepIndex) => {
const liveStatus = liveTask.steps?.[stepIndex]?.status;
if (liveStatus === "done" || liveStatus === "skipped") {
stepExecLog.log(
`Skipping step ${stepIndex} for task ${this.options.taskDetail.id}: live status is ${liveStatus}`,
);
return false;
}
return true;
});
} catch (err) {
stepExecLog.warn(
`Failed to inspect live step status for task ${this.options.taskDetail.id}; continuing from snapshot: ${
err instanceof Error ? err.message : String(err)
}`,
);
return stepIndices;
}
}
/**
* Terminate all active agent sessions and set the aborted flag.
*