fix(FN-7248): skip completed steps on resume
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user