From 41ee8267006a517f170794c7dd5172c5fecb9e4b Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:19:19 +0200 Subject: [PATCH] fix(pipeline): keep an urgent export urgent through a validation override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overriding a failed validation queued the preview at priority 50 regardless. It took its rank from the export's last PREVIEW_ONLY job — but validation runs before the preview stage, so a VALIDATION_FAILED export has never had one, and the missing row fell through to a hardcoded normal priority. An urgent shot lost its urgency at exactly the moment someone was standing over it waiting. Every preview now derives its rank from the export's AE_RENDER priority rather than from a previous preview. That fixes the missing-row case and also removes a slow drift: a preview's priority is already boosted, so re-deriving from it moved an export a step up the queue on every retry. createPreviewJob takes the render priority and the attempt number, so the first build, a retry, a regenerate and an override all rank identically. Co-Authored-By: Claude Opus 5 --- lib/render-pipeline/preview.ts | 73 +++++++++++++++++++++---------- lib/render-pipeline/validation.ts | 25 ++++++----- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/lib/render-pipeline/preview.ts b/lib/render-pipeline/preview.ts index d3ce163..ebfa2b0 100644 --- a/lib/render-pipeline/preview.ts +++ b/lib/render-pipeline/preview.ts @@ -193,13 +193,40 @@ export function previewPriority(renderPriority: number, boost: number, urgentThr return Math.max(0, boosted); } -/** Creates the PREVIEW_ONLY job that follows a successful EXR render. */ +/** + * The priority an export's work runs at: the one its render was queued with. + * + * Every preview for an export derives from this rather than from a previous + * preview job, for two reasons. A preview job may not exist yet — an export + * that failed validation never had one, and reading a missing row used to + * silently drop an urgent export to normal. And deriving from an already- + * boosted preview would boost again on each retry, drifting the export up the + * queue a step at a time. + */ +export async function renderPriorityFor( + tx: Prisma.TransactionClient, + exportId: string +): Promise { + const job = await tx.renderJob.findFirst({ + where: { exportId, type: "AE_RENDER" }, + orderBy: { attempt: "desc" }, + select: { priority: true }, + }); + return job?.priority ?? 50; +} + +/** + * Creates a PREVIEW_ONLY job. `renderPriority` is the export's *render* + * priority; the queue rank is derived from it here so every path — first + * build, retry, regenerate, validation override — ranks identically. + */ export async function createPreviewJob( tx: Prisma.TransactionClient, exportRow: ExportForPreview, manifest: PreviewManifest, maxAttempts: number, - priority: number + renderPriority: number, + attempt = 1 ) { const [boost, urgentThreshold] = await Promise.all([ getConfigNumber("render.previewPriorityBoost"), @@ -210,9 +237,9 @@ export async function createPreviewJob( data: { type: "PREVIEW_ONLY", exportId: exportRow.id, - attempt: 1, + attempt, maxAttempts, - priority: previewPriority(priority, boost, urgentThreshold), + priority: previewPriority(renderPriority, boost, urgentThreshold), manifest: manifest as unknown as Prisma.InputJsonValue, }, }); @@ -421,18 +448,17 @@ export async function retryPreview(exportId: string, actorId?: string) { const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const job = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + const job = await createPreviewJob( + tx, + exportRow, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition(tx, exportRow, "GENERATING_PREVIEW", { type: "USER", @@ -501,18 +527,17 @@ export async function regeneratePreview(exportId: string, input: RegeneratePrevi const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const job = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + const job = await createPreviewJob( + tx, + refreshed, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition( tx, diff --git a/lib/render-pipeline/validation.ts b/lib/render-pipeline/validation.ts index 1c89157..7ae2ffe 100644 --- a/lib/render-pipeline/validation.ts +++ b/lib/render-pipeline/validation.ts @@ -4,7 +4,7 @@ import { db } from "@/lib/db"; import { PipelineError } from "./errors"; import { applyTransition } from "./transitions"; import { getConfigBoolean, getConfigNumber, getConfigValue, getPreviewConfig } from "./config"; -import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia } from "./preview"; +import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia, renderPriorityFor } from "./preview"; /** * Validation engine results (RenderPipeline2 §8). The worker runs the checks — @@ -301,17 +301,20 @@ export async function overrideValidation( const last = await tx.renderJob.findFirst({ where: { exportId, type: "PREVIEW_ONLY" }, orderBy: { attempt: "desc" }, + select: { attempt: true }, }); - const previewJob = await tx.renderJob.create({ - data: { - type: "PREVIEW_ONLY", - exportId, - attempt: (last?.attempt ?? 0) + 1, - maxAttempts, - priority: last?.priority ?? 50, - manifest: manifest as unknown as Prisma.InputJsonValue, - }, - }); + // An export that failed validation has never had a preview job — validation + // runs before the preview stage — so the rank comes from its render, which + // is what keeps an urgent export urgent through an override. Someone is + // usually standing over an urgent shot when they wave a check through. + const previewJob = await createPreviewJob( + tx, + exportRow, + manifest, + maxAttempts, + await renderPriorityFor(tx, exportId), + (last?.attempt ?? 0) + 1 + ); const updated = await applyTransition(tx, exportRow, "GENERATING_PREVIEW", { type: "USER",