ai-chatbot/lib/video-canvas/task-recovery.ts

66 lines
2.5 KiB
TypeScript

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<string, unknown> };
result?: Record<string, any> | null;
};
export function getEnhanceCheckpoint(task: Pick<RecoveryTask, "request" | "result">) {
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<RecoveryTask, "request" | "result">) {
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<RecoveryTask, "request" | "result">) {
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 hasRecoverableCanvasTask(task: RecoveryTask, now = Date.now()) {
const created = Date.parse(task.createdAt);
return (
Boolean(getEnhanceCheckpoint(task) || getVideoCheckpoint(task) || getAudioCheckpoint(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;
}