FN-6478: surface paused workflow graph failures

Surface stranded paused workflow exits as actionable executor failures.

- Treat paused or aborted graph exits as benign only while the live task remains in-progress.
- Preserve terminal/review lifecycle state while recording operator-actionable failure evidence for advanced columns.
- Cover user-paused, pause-aborted, existing-failure, in-progress, in-review, todo, and done column recovery paths.
- Document the workflow lifecycle invariant and add a patch changeset.

Files changed:
 .changeset/fn-6478-paused-workflow-executions.md   |   5 +
 docs/architecture.md                               |   1 +
 .../engine/src/__tests__/executor-recovery.test.ts | 283 +++++++++++++++++++++
 packages/engine/src/executor.ts                    |  30 ++-
 4 files changed, 315 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-6478

Fusion-Task-Lineage: 219d8612-6604-4dbc-9a3a-a1c7837419c1
This commit is contained in:
gsxdsm
2026-06-15 02:20:45 -07:00
parent de8f871b4d
commit bc6dfd386e
4 changed files with 315 additions and 4 deletions

View File

@@ -6317,11 +6317,33 @@ export class TaskExecutor {
this.options.stuckTaskDetector?.untrackTask(task.id);
try {
const live = await this.store.getTask(task.id);
// A paused/aborted implementation is not a graph failure — leave the
// pause machinery in charge instead of parking the task in review.
if (live.paused || this.pausedAborted.has(task.id)) {
// A paused/aborted implementation is not a graph failure while the task
// is still in-progress — leave the pause machinery in charge instead of
// parking the task in review.
const pausedAborted = this.pausedAborted.has(task.id);
if (live.paused || pausedAborted) {
/*
FNXC:WorkflowLifecycle 2026-06-15-01:45:
FN-6478: a graph exit during an in-progress pause is recoverable by explicit unpause, but the same exit after the task has already left in-progress strands the workflow graph. Preserve userPaused and autoMerge:false review parking; surface non-in-progress paused exits as operator-actionable failures without moving the task backward or re-enqueueing execution.
*/
const pauseProvenance = live.userPaused
? "explicit user pause"
: pausedAborted
? "engine abort during pause/resume"
: "task pause";
if (live.column !== "in-progress") {
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1] ?? "unknown";
const message = `Workflow graph failure surfaced after paused ${pauseProvenance} in '${live.column}' at node '${failedNode}' — operator action required; retry or explicitly unpause/resume after inspecting the task`;
executorLog.warn(`${task.id}: ${message}`);
await this.store.logEntry(task.id, message, undefined, this.getRunContextFor(task.id));
if (live.column !== "done" && live.column !== "archived" && live.status == null && live.error == null) {
await this.store.updateTask(task.id, { error: message, status: "failed" }, this.getRunContextFor(task.id));
}
await this.persistTokenUsage(task.id);
return;
}
const benignMessage = "Workflow graph run ended while task is paused — pause state preserved";
executorLog.log(`${task.id}: ${benignMessage}`);
executorLog.log(`${task.id}: ${benignMessage} (${pauseProvenance})`);
await this.store.logEntry(task.id, benignMessage, undefined, this.getRunContextFor(task.id));
return;
}