diff --git a/desktop/src/features/workflows/triggerOperations.test.mjs b/desktop/src/features/workflows/triggerOperations.test.mjs index 7fb669afd8..fb2f3e3352 100644 --- a/desktop/src/features/workflows/triggerOperations.test.mjs +++ b/desktop/src/features/workflows/triggerOperations.test.mjs @@ -58,6 +58,7 @@ test("production owner retains exact signed payload after committed response los release(); await assert.rejects(first, /response lost/); assert.equal(owner.state(key).status, "error"); + assert.equal(owner.state(key).failurePhase, "submit"); const result = await owner.run("workflow", scope); assert.equal(result.runId, "run-1"); assert.equal(prepared, 1); @@ -119,6 +120,10 @@ test("preflight rejection retries preparation without inventing a submitted even assert.fail("preflight failure must not publish"), }); await assert.rejects(owner.run("workflow", scope), /stale revision/); + assert.equal( + owner.state(owner.key("workflow", scope)).failurePhase, + "prepare", + ); await assert.rejects(owner.run("workflow", scope), /stale revision/); assert.equal(tries, 2); }); diff --git a/desktop/src/features/workflows/triggerOperations.ts b/desktop/src/features/workflows/triggerOperations.ts index 55670288a3..1d2fd1676b 100644 --- a/desktop/src/features/workflows/triggerOperations.ts +++ b/desktop/src/features/workflows/triggerOperations.ts @@ -8,6 +8,7 @@ import type { RelayEvent, TriggerWorkflowResponse } from "@/shared/api/types"; export type TriggerState = { status: "idle" | "pending" | "error" | "success"; error?: string; + failurePhase?: "prepare" | "submit"; result?: TriggerWorkflowResponse; }; const idle: TriggerState = { status: "idle" }; @@ -61,7 +62,8 @@ export class WorkflowTriggerOperations { let op = this.operations.get(key); if (op?.inFlight) return op.inFlight; // A settled success means the next explicit Trigger is a distinct run. - // An error means Retry of the same event unless the user explicitly opts out. + // Submission errors retry the same event unless the user explicitly opts out. + // Preparation errors have not published anything and may prepare again. if (!op || newRun || op.state.status === "success") { if (!op && this.operations.size >= 256) { for (const [oldKey, old] of this.operations) { @@ -99,6 +101,7 @@ export class WorkflowTriggerOperations { operation.state = { status: "error", error: error instanceof Error ? error.message : String(error), + failurePhase: operation.event ? "submit" : "prepare", }; throw error; } finally { diff --git a/desktop/src/features/workflows/ui/WorkflowTriggerFeedback.tsx b/desktop/src/features/workflows/ui/WorkflowTriggerFeedback.tsx index b915299728..0389ac5cc7 100644 --- a/desktop/src/features/workflows/ui/WorkflowTriggerFeedback.tsx +++ b/desktop/src/features/workflows/ui/WorkflowTriggerFeedback.tsx @@ -34,6 +34,19 @@ export function WorkflowTriggerFeedback({ Run created: {state.result?.runId}
); + if (state.failurePhase === "prepare") + return ( +Workflow run was not started. {state.error}
+Retry prepares a new request before submitting it.
+ +