fix: recover matching durable image video submission receipts

This commit is contained in:
Wenxin Duan
2026-09-15 01:14:25 +08:00
parent 40e03e393c
commit cc80265bb2
2 changed files with 59 additions and 3 deletions
+17 -3
View File
@@ -6,6 +6,7 @@ import { HarnessError } from "../errors/harness-error.js";
import { ImageVideoDraftSchema, recordSchema, taskIdSchema, validationSchema, type ImageVideoDraft, type ImageVideoPublicTask, type ImageVideoRecord, type ImageVideoRemote, type ImageVideoValidation } from "./contracts.js";
const locks = new Map<string, Promise<unknown>>();
const submissionReceiptSchema = recordSchema.pick({ version: true, id: true, revision: true, requestHash: true, jobId: true }).required().strict();
export const sha256 = (bytes: Buffer | string) => createHash("sha256").update(bytes).digest("hex");
const fail = (code: string, message: string): never => { throw new HarnessError(code, message); };
export type ImageVideoOptions = { root: string; attachments: Pick<AttachmentStore, "get" | "openNormalized">; remote: ImageVideoRemote; collect: (url: string, directory: string, duration: number) => Promise<ImageVideoValidation> };
@@ -57,8 +58,8 @@ export class ImageVideoService {
async submit(sessionId: string, id: string): Promise<ImageVideoPublicTask> {
return this.lock(id, async () => {
const r = await this.read(id); this.owner(r, sessionId);
if (r.jobId) return this.public(r);
if (r.state === "unknown_submission") return fail("I2V_UNKNOWN_SUBMISSION", "Submission outcome unknown; no safe retry or new billing request is allowed");
if (r.jobId) return this.public(r);
if (r.state !== "awaiting_submission" || !r.assetId) return fail("I2V_NOT_READY", "Reference import must be ready before generation approval");
await this.options.remote.preflight(); await this.reference(r);
r.state = "submitting"; await this.save(r);
@@ -76,7 +77,7 @@ export class ImageVideoService {
async refresh(id: string): Promise<ImageVideoPublicTask> {
return this.lock(id, async () => {
const r = await this.read(id);
if (r.state === "ready" || r.state === "failed" || Date.now() - (r.lastPollAt ?? 0) < 30_000) return this.public(r);
if (r.state === "unknown_submission" || r.state === "ready" || r.state === "failed" || Date.now() - (r.lastPollAt ?? 0) < 30_000) return this.public(r);
if (!r.jobId && !(r.upload && ["importing", "unknown_upload"].includes(r.state))) return this.public(r);
r.lastPollAt = Date.now(); await this.save(r);
try {
@@ -118,6 +119,15 @@ export class ImageVideoService {
}
private async reference(r: ImageVideoRecord) { await this.regular(join(this.root, r.id, "reference")); const bytes = await readFile(join(this.root, r.id, "reference")); if (sha256(bytes) !== r.referenceHash) fail("I2V_MEDIA_INVALID", "Reference changed after draft"); return bytes; }
private async regular(path: string) { const info = await lstat(path); if (!info.isFile() || info.isSymbolicLink()) fail("I2V_RECORD_INVALID", "Unsafe registered file"); }
private async matchingSubmissionReceipt(r: ImageVideoRecord): Promise<string | undefined> {
const path = join(this.root, r.id, "submission-receipt.json");
try {
await this.regular(path);
const receipt = submissionReceiptSchema.parse(JSON.parse(await readFile(path, "utf8")));
if (receipt.id !== r.id || receipt.requestHash !== r.requestHash || receipt.revision !== r.revision || !receipt.jobId.trim() || (r.jobId !== undefined && r.jobId !== receipt.jobId)) return undefined;
return receipt.jobId;
} catch { return undefined; } // Preserve absent, malformed and conflicting evidence for manual reconciliation.
}
private async read(id: string): Promise<ImageVideoRecord> {
if (!taskIdSchema.safeParse(id).success) return fail("I2V_TASK_NOT_FOUND", "Task not found");
const path = join(this.root, id, "record.json");
@@ -126,7 +136,11 @@ export class ImageVideoService {
try {
await this.regular(path); const r = recordSchema.parse(JSON.parse(await readFile(path, "utf8")));
if (r.id !== id || r.requestHash !== sha256(JSON.stringify(r.draft))) throw new Error("Binding mismatch");
if (r.state === "submitting") { r.state = "unknown_submission"; await this.save(r); }
if (r.state === "submitting") {
const jobId = await this.matchingSubmissionReceipt(r);
if (jobId !== undefined) { r.jobId = jobId; r.state = "queued"; delete r.lastPollAt; delete r.error; await this.save(r); }
else { r.state = "unknown_submission"; await this.save(r); }
}
if (["initializing_upload", "uploading", "finalizing"].includes(r.state)) { r.state = "unknown_upload"; await this.save(r); }
return r;
} catch { return fail("I2V_RECORD_INVALID", "Task record is invalid; manual recovery required"); }