import type { SmartEditCheckpoint } from "./smart-edit/execute"; import { audioCheckpointSchema } from "./audio-contract"; import { videoCheckpointSchema } from "./video-task-lifecycle"; // Resume only server-recorded provider jobs. A lost request without an upstream ID is never resubmitted. export type RecoveryTask = { status: string; createdAt: string; updatedAt?: string; attempts?: number; request: { kind: string; params: Record }; result?: Record | null; }; export function getEnhanceCheckpoint( task: Pick ) { const upstream = task.result?.upstreamTask; if ( task.request.kind !== "image-operation" || task.request.params.operation !== "enhance" || upstream?.operation !== "enhance" || typeof upstream.taskId !== "string" || !upstream.taskId.trim() || upstream.sourceImageUrl !== task.request.params.imageUrl ) return null; return { operation: "enhance" as const, taskId: upstream.taskId, sourceImageUrl: String(upstream.sourceImageUrl), }; } export function getVideoCheckpoint( task: Pick ) { if (task.request.kind !== "video") return null; const parsed = videoCheckpointSchema.safeParse(task.result?.upstreamTask); if (!parsed.success) return null; const preferred = task.request.params.preferredModelRef; if ( preferred && preferred !== "auto" && preferred !== parsed.data.modelRef && preferred !== parsed.data.modelId ) return null; return parsed.data; } export function getAudioCheckpoint( task: Pick ) { if (task.request.kind !== "audio") return null; const parsed = audioCheckpointSchema.safeParse(task.result?.upstreamAudio); return parsed.success && parsed.data.modelRef === task.request.params.modelRef ? parsed.data : null; } export function getSmartEditCheckpoint( task: Pick ): SmartEditCheckpoint | null { const value = task.result?.smartEdit; if ( task.request.kind !== "smart-edit" || value?.operation !== "smart-edit" || value.action !== task.request.params.operation || !["local", "analyzed", "selected", "remote-render"].includes(value.phase) ) return null; if ( value.phase === "remote-render" && (value.action !== "render" || !/^[a-f0-9]{64}$/.test(value.remote?.id || "") || !/^[a-f0-9]{64}$/.test(value.remote?.endpointHash || "")) ) return null; return value as SmartEditCheckpoint; } export function hasRecoverableCanvasTask(task: RecoveryTask, now = Date.now()) { const created = Date.parse(task.createdAt); return ( Boolean( getEnhanceCheckpoint(task) || getVideoCheckpoint(task) || getAudioCheckpoint(task) || getSmartEditCheckpoint(task) || getUpscaleCheckpoint(task) ) && Number.isFinite(created) && now >= created && now - created <= 86400000 && (task.attempts || 0) < 20 ); } export function canResumeCanvasTask(task: RecoveryTask, now = Date.now()) { if (task.status !== "interrupted" || !hasRecoverableCanvasTask(task, now)) return false; const updated = Date.parse(task.updatedAt || ""); const delay = Math.min( 300000, 30000 * 2 ** Math.min(4, Math.max(0, (task.attempts || 1) - 1)) ); return Number.isFinite(updated) && now - updated >= delay; } export function getUpscaleCheckpoint( task: Pick ) { const value = task.result?.upscale?.remote; return task.request.kind === "video-upscale" && /^[a-f0-9]{64}$/.test(value?.id || "") && /^[a-f0-9]{64}$/.test(value?.endpointHash || "") ? (value as { id: string; endpointHash: string }) : null; }