From 30e2987d5285a71a7f5bf5cd426c0ec32e80d5bb Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Mon, 26 Oct 2026 21:35:01 +0000 Subject: [PATCH] fix(producer): retry segmented capture on screenshot after a sequential stall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit captureSegmentFrames races each frame through raceAgainstStall, which throws SequentialCaptureStallError. The segmented catch only retried drawElement capture, verification and renderer-stall errors, so a drawElement frame that hung without the engine's frame timeout firing failed the whole render, where the streaming path retries the same error on a fresh screenshot session. Retry it when the plan did not already force screenshot. The failure kind is draw_element_capture, which keeps segmenting and forces screenshot; capture_failure would replan to plain streaming, which this catch cannot drive. deFallbackReason stays "capture_error", the value streaming records for a sequential stall. — Via Co-Authored-By: Claude Opus 5.5 (1M context) --- .../renderOrchestrator.segmentedDeRetry.test.ts | 9 +++++++++ packages/producer/src/services/renderOrchestrator.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/producer/src/services/renderOrchestrator.segmentedDeRetry.test.ts b/packages/producer/src/services/renderOrchestrator.segmentedDeRetry.test.ts index c8fa381a9e..ccc611b218 100644 --- a/packages/producer/src/services/renderOrchestrator.segmentedDeRetry.test.ts +++ b/packages/producer/src/services/renderOrchestrator.segmentedDeRetry.test.ts @@ -190,6 +190,14 @@ function render(resumeSegments = false): Promise { return executeRenderJob(job, root, join(root, "output.mp4")); } +function sequentialStall(): Error { + const err = new Error( + "[Render] Sequential drawElement capture stalled: no frame progress for 1000ms (stuck at frame 640/1200).", + ); + err.name = "SequentialCaptureStallError"; + return err; +} + function stalledRenderer(): Error { const err = new Error("drawElement frame exceeded 5000ms (renderer stopped scheduling)"); err.name = "DeFrameTimeoutError"; @@ -200,6 +208,7 @@ it.each([ ["a suspect small frame", () => new DrawElementCaptureError(640, "suspect small frame")], ["no cached paint record", () => new DrawElementCaptureError(640, "No cached paint record")], ["a stalled renderer", stalledRenderer], + ["a sequential capture stall", sequentialStall], [ "a failed self-verification", () => new DrawElementVerificationError("drawElement PSNR too low", { kind: "psnr" }), diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 0816ddbca5..21f5ebbbd8 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -4741,8 +4741,13 @@ async function executeRenderPipeline(input: { } catch (err) { const isVerifyError = isDrawElementVerificationError(err); const isDeStall = isDeRendererStallError(err); + const isUnforcedSequentialStall = + isSequentialCaptureStallError(err) && !capturePlan.forceScreenshot; if ( - (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall) || + (!isVerifyError && + !isDrawElementCaptureError(err) && + !isDeStall && + !isUnforcedSequentialStall) || err instanceof RenderCancelledError || executionSignal?.aborted === true ) { @@ -4759,7 +4764,9 @@ async function executeRenderPipeline(input: { deFallbackFrameIndex = t.frameIndex; deFallbackThresholdDb = t.thresholdDb; log.warn( - "[Render] drawElement capture failed on the segmented path; re-rendering via screenshot", + isUnforcedSequentialStall + ? "[Render] segmented capture stalled; retrying on a fresh screenshot session" + : "[Render] drawElement capture failed on the segmented path; re-rendering via screenshot", { error: err instanceof Error ? err.message : String(err), },