// A serializable description of a job, sufficient to re-launch (replay) it. // // It is captured at job-creation time (stored on the JobRecord and in the // .meta.json sidecar), which is what lets Retry re-run a job — including one // that has been evicted from the in-memory registry. The dispatcher in // editor/app/jobs/runJobSpec.ts maps a spec back to the server action that runs // it. // // Re-running RE-DERIVES the work from the channel's CURRENT state rather than // replaying a frozen list: bucket jobs store only the snapshot bucket category // and the dispatcher reads the live bucket members at run time, so e.g. // "retry partial downloads on cornbreadman" always acts on whatever is partial // now. Flag-style parameters (queue, audio format, abort-on-error…) are captured // verbatim in `params`. // The snapshot bucket categories whose current members a replayed job can be // re-derived from. A subset of ChannelSnapshot["buckets"] — only the buckets the // bucket-style actions (retry-bucket / whisper-bucket) actually operate on. export type ReplayBucket = | "partialDownloads" | "noTranscript" | "downloadedNoTranscript" | "incompleteTranscript" | "shortAudio" | "needsCookies" // Replace-auto-captions lane: ASR-only without audio (fetch audio), ASR-only // with audio (transcribe), and the kept-VTT backups (manual purge). | "autoSubsOnly" | "downloadedAutoSubsOnly" | "supersededAutoSubs"; export type JobSpec = { kind: string; slug: string; // Present only for bucket jobs: the snapshot bucket whose current members the // job should operate on when re-run. bucket?: ReplayBucket; // Flag-style parameters captured verbatim (queueKey, reverse, audioFormat, // strictAudioFormat, abortOnError, ignoreArchive, handlingOverride, // targetFormat, shardTotal, shardIndex…). All JSON-serializable. params?: Record; }; const REPLAY_BUCKETS: ReadonlySet = new Set([ "partialDownloads", "noTranscript", "downloadedNoTranscript", "incompleteTranscript", "shortAudio", "needsCookies", "autoSubsOnly", "downloadedAutoSubsOnly", "supersededAutoSubs", ]); // Is this bucket name one a replayed job can be re-derived from? Exported so a // caller naming a bucket (the ops API's retry-bucket route) can decide whether // the job is replayable without re-spelling the set. export function isReplayBucket(value: unknown): value is ReplayBucket { return typeof value === "string" && REPLAY_BUCKETS.has(value); } // Defensive parse for a spec read back from JSON (the .meta.json sidecar). // Returns null on anything malformed so a hand-edited or stale file can't // crash a reader. Mirrors the tolerance of readJobMeta / readWorkerDefaults. export function parseJobSpec(raw: unknown): JobSpec | null { if (!raw || typeof raw !== "object") return null; const r = raw as Record; if (typeof r.kind !== "string" || !r.kind) return null; if (typeof r.slug !== "string" || !r.slug) return null; const spec: JobSpec = { kind: r.kind, slug: r.slug }; if (r.bucket !== undefined) { if (typeof r.bucket !== "string" || !REPLAY_BUCKETS.has(r.bucket)) { return null; } spec.bucket = r.bucket as ReplayBucket; } if (r.params && typeof r.params === "object" && !Array.isArray(r.params)) { spec.params = r.params as Record; } return spec; }