Skip to content

fix(producer): type video extraction failures - #2776

Merged
jrusso1020 merged 3 commits into
mainfrom
fix/video-extraction-failure-retry
Jul 26, 2026
Merged

fix(producer): type video extraction failures#2776
jrusso1020 merged 3 commits into
mainfrom
fix/video-extraction-failure-retry

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

Summary

  • classify per-source video download/probe/decode/extraction failures with a bounded taxonomy and safe producer-facing summaries
  • add candidate-only, at-most-one transient retry with cleanup and retry telemetry
  • preserve default engine/producer behavior when the policy is off
  • carry allowlisted extraction error codes through blocking JSON and SSE responses

Stack

Depends on #2774 for atomic remote downloads and its single owned download retry. This PR is intentionally based on fix/atomic-video-download-retry; rebase/change the base to main after #2774 merges.

Default compatibility

HF_VIDEO_EXTRACTION_FAILURE_MODE defaults to off and forces maxTransientRetries=0.

With the feature off:

  • metadata probe failures keep the legacy Promise rejection
  • grouped extraction keeps the existing grouped-to-direct fallback
  • no new producer failure gate is enforced
  • render-plan schema, Plan v1 artifacts, chunk routing, and distributed execution are unchanged

Typed metadata aggregation is explicit and enabled only by the candidate enforce lane.

Retry ownership

  • remote downloads: exactly one retry owned by fix(engine): make remote video downloads atomic #2774
  • metadata/FFmpeg extraction: at most one retry only when HF_VIDEO_EXTRACTION_MAX_RETRIES=1
  • invalid, missing, rejected, out-of-range, zero-output, cancellation, and unknown/internal failures do not retry
  • non-finite or invalid runtime retry budgets fail closed to zero
  • the superset optimization is never retried; on failure it preserves direct-member fallback, and only the individual ranges can use the bounded retry
  • retry counters increment when a retry is scheduled, including exhausted retries

The internal sidecar and Experiment Framework must treat both exhausted stage codes as workflow-terminal after the producer-local budget. Candidate enforcement must not be enabled until those companion mappings are deployed, or Temporal can multiply producer attempts.

Failure contract

  • VIDEO_SOURCE_UNRENDERABLE: at least one deterministic/unknown source failure
  • VIDEO_EXTRACTION_FAILED: all source failures are transient but the producer-local budget is exhausted

Only the allowlisted code and kind/count summaries cross JSON/SSE. Raw diagnostics remain engine-local because they may contain signed URLs or local paths.

Rollout

  1. merge and deploy with stable/candidate both off
  2. candidate observe, retries 0
  3. candidate observe, retries 1
  4. deploy internal + EF terminal transport mappings
  5. candidate enforce, retries 1
  6. keep stable off until success delta, retry counts, extraction latency, CPU/disk, and queue backlog are acceptable

Validation

  • engine focused suites: 105 passed
  • producer focused suites: 15 passed
  • full engine suite: 1,176 passed, 3 skipped
  • full producer unit lane: 32 Vitest files / 393 tests plus all classified Bun unit tests
  • engine and producer typechecks passed
  • oxlint, oxfmt, Fallow, tracked-artifact, and commit hooks passed
  • independent review: approved for merge default-off; candidate enforcement held on companion transport rollout

@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from aa2b128 to 13e1704 Compare July 26, 2026 17:39
@jrusso1020
jrusso1020 force-pushed the fix/atomic-video-download-retry branch from 5f05376 to d80be86 Compare July 26, 2026 17:39

jrusso1020 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial R1 review — hyperframes PR #2776

Head: 13e17046c698a03134d5e33afe68b3f0af01c78d
Title: fix(producer): type video extraction failures
Base: fix/atomic-video-download-retry (child of #2774)
Delta: +1009 / -59, 12 files (largest in the stack)


Verdict

APPROVE with 2 nits — the taxonomy + producer-safe error surface is well-designed, the primary lens (multi-shape throw discipline → wire discriminator) is honored on the JSON/SSE producer surface, and the entire new failure gate is default-off with a bounded 0|1 retry ceiling. The known wire-drift on the Step Functions + GCP Cloud Run boundaries is explicitly documented in the PR body as a rollout dependency ("companion transport mappings" gate the enforce lane), not a defect in this PR.

No P0. No P1. Two P2 nits below.


Adversarial pass — evidence walk

1. Multi-shape throw discipline (PRIMARY LENS)

Discriminator on the producer HTTP surface is .code, not .name. extractSafeRenderErrorCode in packages/producer/src/server.ts reads err.code, allowlists against a bounded Set of two literals, and never parses .message. The three throw shapes it must accept converge cleanly:

Shape Test coverage Assertion form
Class-instance throw (new VideoExtractionStageError(...)) server.errorCode.test.ts:7-16 .toBe("VIDEO_SOURCE_UNRENDERABLE") / .toBe("VIDEO_EXTRACTION_FAILED")
Code-only bag throw ({code: "VIDEO_SOURCE_UNRENDERABLE"}) server.errorCode.test.ts:19-22 .toBe("VIDEO_SOURCE_UNRENDERABLE")
Message-embedded / name-alias / arbitrary code server.errorCode.test.ts:25-30 .toBeUndefined() (rejected)
  • Assertions use the canonical string constant ("VIDEO_SOURCE_UNRENDERABLE"), not err.name or err.code — no trivial-pass smell.
  • Message-embedded probe (new Error("failed [VIDEO_SOURCE_UNRENDERABLE; secret=/tmp/x]")) is explicitly rejected — the wire never parses message text, so a leaked signed-URL/path in a stringified error can never smuggle a synthetic code.
  • The one "shape" not tested is a name-alias throw (new Error(); e.name = "VIDEO_SOURCE_UNRENDERABLE"). This is correct-by-design: extractSafeRenderErrorCode inspects .code not .name, so name-alias is not a valid discriminator on this wire.

VideoSourceExtractionError (engine layer) uses a duck-typed sentinel (hyperframesVideoSourceExtractionError: true) for cross-realm safe detection via isVideoSourceExtractionError — reasonable pattern; contained.

buildVideoExtractionStageError fails closed on legacy failure shapes with no .kind / no .retryable: kind defaults to "internal", retryable defaults to false (via .every((f) => f.retryable === true) — undefined coerces to non-retryable). extractVideosStage.test.ts:151-168 pins this.

2. Dispatch chain verification

Traced end-to-end for the two flow paths that reach a wire boundary:

A. Movio / standalone producer HTTP → JSON/SSE. extractVideosStage.ts sets failureToEnforcerenderOrchestrator.ts:2147 throws after checkpoint → server.ts catch handler calls extractSafeRenderErrorCode(error)code field appears in SSE type: "error" payload and in the failure JSON. Sanitized: the code is the allowlisted literal, never the raw .message. This wire is CORRECT.

B. Distributed plan → AWS Lambda / GCP Cloud Run → orchestrator. plan.ts:951 throws VideoExtractionStageError. This propagates out of the Lambda / Cloud Run handler, whose retry discriminators are:

  • packages/aws-lambda/src/cdk/HyperframesRenderStack.ts:196 NON_RETRYABLE_PLAN — matches on Step Functions ErrorEquals, which is Error.name
  • packages/aws-lambda/src/handler.ts:145 normalizeTerminalErrorName — rewrites .name := .code for a hand-curated list (PLAN_PROTOCOL_UNSUPPORTED, PLAN_TOO_LARGE, PLAN_V2_INTEGRITY_UNRECOVERABLE)
  • packages/gcp-cloud-run/src/server.ts:944 NON_RETRYABLE_ERROR_NAMES — dispatches on err.name

None of these three lists include VideoExtractionStageError, VIDEO_SOURCE_UNRENDERABLE, or VIDEO_EXTRACTION_FAILED. So if enforce mode is enabled today, VideoExtractionStageError (name is literally "VideoExtractionStageError") will:

  • Never be normalized in normalizeTerminalErrorName
  • Never match NON_RETRYABLE_PLAN → Step Functions retries the plan step up to 4× with exponential backoff
  • Never match NON_RETRYABLE_ERROR_NAMES → Cloud Run returns 500 → Cloud Workflows retries

This is the exact wire-drift trap the primary lens is designed to catch — but the PR body pre-empts it verbatim:

The internal sidecar and Experiment Framework must treat both exhausted stage codes as workflow-terminal after the producer-local budget. Candidate enforcement must not be enabled until those companion mappings are deployed, or Temporal can multiply producer attempts.

The rollout section labels deploy internal + EF terminal transport mappings as step 4 (before enforce is turned on at step 5). Given the default is off, no throw actually reaches the wire in production behavior today — the drift is a rollout dependency, not a landed defect. Not a finding I would block on. See Nit A below for the peripheral suggestion.

3. NON_RETRYABLE classification of new codes

Not applicable — the class deliberately splits deterministic vs exhausted-transient into two distinct code values (VIDEO_SOURCE_UNRENDERABLE vs VIDEO_EXTRACTION_FAILED), each carrying an explicit .retryable boolean. The producer-local retry policy is respected in the engine (runVideoExtractionWithRetry throws when !classified.retryable || retries >= budget). Correct.

4. observability.ts +2 lines

Adds transientRetries?: number to RenderExtractionObservability. Matches: (a) phaseBreakdown.transientRetries accumulated in extractAllVideoFrames and runExtractVideosStage (HDR probe retries), (b) piped through in renderOrchestrator.ts:2150 and summarizeExtractionObservability at line 234, (c) ?? null at the final observability emission. The label is derived directly from the actual retry-count call sites — no observability-label-vs-actual-path drift.

5. server.ts +21 lines

Two-site: (a) extractSafeRenderErrorCode implementation + SAFE_RENDER_ERROR_CODES allowlist, (b) two catch handlers append errorCode to the SSE write*Failure and to the JSON error body. Uses the allowlisted constant not err.name. Reads only from .code — never parses message text. Explicit test at server.errorCode.test.ts:25-30 pins the "no message parsing" invariant. No wire-drift here.

6. plan.ts +1 line

if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;

Textually correct — throws only if the stage populated failureToEnforce (which only happens under HF_VIDEO_EXTRACTION_FAILURE_MODE=enforce). Preserves default behavior (off → null → no throw). The wire-boundary side of this throw is the dependency documented in the PR body (see §2B / Nit A).

7. Test coverage of the actual paths

  • videoFrameExtractor.test.ts +157: exercises classifyVideoExtractionError (404 → non-retryable, 503 → retryable), runVideoExtractionWithRetry (one retry with partial-cleanup verification via existsSync(partialPath).toBe(false)), cancellation before operation (abortedAttempts === 0), non-retryable failures (deterministic + budget=0), and NaN budget (Number.isFinite fail-closed). Also two end-to-end assertions with the real ffmpeg fixture: media-start-beyond-duration throws media_start_out_of_range before invoking FFmpeg, and legacy metadata rejection is preserved unless collectProbeFailures: true is explicitly enabled. All exercise the real paths, no mock trivialization.
  • extractVideosStage.test.ts +151: covers resolveVideoExtractionPolicy default-off, assertVideoExtractionSucceeded success, deterministic message-sanitization (.not.toContain("/tmp/"), .not.toContain("Signature")), exhausted-transient path collapses duplicate kind, and the legacy-failure fail-closed path.
  • urlDownloader.test.ts +12: verifies the new onTransientRetry callback fires once with the classified UrlDownloadError when a 503 is retried. The producer-side effect (recording the retry into transientRetries counter) is not directly asserted here but is downstream of this callback.

8. Retry budget interaction

  • Downloader has its own maxTransientRetries = 1 inside downloadWithRetry (unchanged by this PR).
  • Extraction wrapper adds another runVideoExtractionWithRetry layer with its own 0|1 budget.
  • Phase 1 (download): only urlDownloader's internal retry applies — not wrapped in the extraction wrapper. Max 2 HTTP attempts.
  • Phase 2/3 (probe/extract): wrapped in runVideoExtractionWithRetry — up to 2 ffprobe / ffmpeg invocations per source, with output-dir cleanup between attempts.
  • boundedTransientRetryBudget(NaN | undefined | negative | 0) = 0 — verified by test.
  • boundedTransientRetryBudget(1 | 5 | Infinity) = 1 — clamped. Total budget is tightly bounded.

9. Freshness / mergeability

mergeable_state: unstable per gh api /repos/heygen-com/hyperframes/pulls/2776. Not a blocker for review — CI settling on the stack is expected while #2774 (parent) shifts.


Findings

Nit A (P2, documented rollout dependency, not a landed defect)

The Step Functions NON_RETRYABLE_PLAN list (HyperframesRenderStack.ts:196), the AWS Lambda handler's normalizeTerminalErrorName (handler.ts:145), and the Cloud Run NON_RETRYABLE_ERROR_NAMES (gcp-cloud-run/src/server.ts:892) each currently key off Error.name — none contains VideoExtractionStageError nor either of the two new codes. If HF_VIDEO_EXTRACTION_FAILURE_MODE=enforce is enabled before the "companion transport mappings" ship (per the PR body's rollout §4), a deterministic VIDEO_SOURCE_UNRENDERABLE throw from plan.ts will be retried by Step Functions/Workflows (4× with backoff) instead of failing fast.

The PR body explicitly gates the enforce rollout on this dependency, so treating this as a P2 note not a blocker. Two low-cost hardenings would kill the trap statically:

  1. In handler.ts:normalizeTerminalErrorName, extend the code list to include "VIDEO_SOURCE_UNRENDERABLE" and "VIDEO_EXTRACTION_FAILED" so .name := .code rewrite happens automatically once the state-machine list catches up.
  2. Consider naming the class VIDEO_SOURCE_UNRENDERABLE (or setting this.name = code in the ctor) so Error.name and code align — that also matches the existing pattern for PLAN_HASH_MISMATCH which sets error.name = "PLAN_HASH_MISMATCH" at every throw site (handler.ts:693, 840, 848). This would let the state-machine list use either the class name or the code with equivalent effect, closing the drift lane entirely.

Both are follow-ups; not a change to this PR.

Nit B (P2, minor consistency)

In extractVideosStage.ts:283-292, the HDR-probe enforce branch throws a fresh VideoExtractionStageError synchronously from inside the Promise.all(...)before any extraction-stage telemetry checkpoint is emitted. Compare against the frame-extraction path where failureToEnforce is captured and re-thrown by renderOrchestrator.ts:2151 after the logEvent("extraction_finalized", ...) observability row is written (line 2137-2149).

The comment on failureToEnforce explicitly says: Callers throw this only after their extraction telemetry checkpoint has been emitted. The HDR probe path violates that invariant — enforce-mode probe failures cost the observability row.

Two remediations:

  • Preferred: capture the classified HDR probe failure into a similar hdrProbeFailureToEnforce: VideoExtractionStageError | null variable, populate it in the catch, and defer the throw to runExtractVideosStage's return path (after videoExtractMs finalization).
  • Or: at minimum, document the exception in the comment on failureToEnforce so future readers know HDR probe throws pre-checkpoint by design.

Follow-up. Doesn't block this PR — the enforce lane hasn't shipped yet.


Observations (informational, no ask)

  • classifyVideoExtractionError HTTP status fallback (line 175 onward in videoFrameExtractor.ts) classifies 401/403 as source_rejected / non-retryable. In the signed-URL / short-TTL world, 401 is sometimes a transient class ("signed URL expired between issue and probe"). But the primary path uses the typed UrlDownloadError.kind from urlDownloader, not this regex fallback; this path only fires for legacy raw-Error propagations. Fine.
  • applyVideoExtractionFailurePolicy observe mode logs a warning but does not throw. Its log-only side effect isn't directly asserted by unit tests (log spy). Given the mode is a deliberate no-op-with-telemetry canary, an integration test that resolves resolveVideoExtractionPolicy({HF_VIDEO_EXTRACTION_FAILURE_MODE: "observe"}) and asserts the log line would tighten the loop, but not required.
  • Sorted failure summaries (buildVideoExtractionStageError) — kinds sorted alphabetically for deterministic error message shape. Assertions in extractVideosStage.test.ts:117-120 verify this. Good.
  • extractDirectMiss partial-dir cleanup now also runs rmSync(partialDir, { recursive: true, force: true }) before the first attempt (not just before retries). Combined with the on-retry cleanup + mkdir, ensures each attempt starts against a clean directory. Small correctness upgrade even for the default maxTransientRetries=0 path.

Summary paragraph

The PR types video-extraction failures into a bounded taxonomy, plumbs a producer-safe .code-based discriminator across the JSON/SSE render surface, and gates every behavioral change (typed aggregation + at-most-one retry + enforce failure gate) behind either HF_VIDEO_EXTRACTION_FAILURE_MODE ∈ {observe,enforce} or an explicit collectProbeFailures: true option — the default is byte-for-byte legacy. Multi-shape throw discipline is honored on the producer HTTP wire (class-instance + code-only throws converge on the same allowlisted code; message-embedded/arbitrary codes are rejected; assertions use canonical string constants). The trans-cloud wire-drift risk (Step Functions/Cloud Run's .name-keyed dispatch is unaware of VideoExtractionStageError) is real but explicitly documented in the PR body as a rollout dependency (enforce lane is gated on companion transport mappings), so it doesn't affect the default-off ship state. Two P2 nits: (A) the AWS Lambda normalizeTerminalErrorName and Step Functions/Cloud Run non-retryable lists are follow-up work called out in the rollout plan but easy to pre-land; (B) HDR-probe enforce failures throw before the extraction telemetry checkpoint, violating the invariant documented on failureToEnforce. APPROVE for merge default-off.

— Review by Via

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 13e17046c698a03134d5e33afe68b3f0af01c78d.

Stable-by-default plumbing is airtight. The default-off path is byte-identical to legacy behavior: resolveVideoExtractionPolicy fails closed on unrecognized values, boundedTransientRetryBudget clamps NaN/negatives/floats to 0, HF_VIDEO_EXTRACTION_MAX_RETRIES accepts only "1" after .trim(), and every hot path branches on maxTransientRetries === 0 ? directCall() : runVideoExtractionWithRetry() so the retry wrapper doesn't rewrap the call graph unless the candidate lane is enabled. The taxonomy is complete and doesn't leak — VideoExtractionStageError.message is built from ${kind}=${count} pairs only, and the extractSafeRenderErrorCode allowlist consciously refuses to parse arbitrary code strings or scan the message (the "does not forward arbitrary codes or parse message text" test pins that). The assertVideoExtractionSucceeded "signed URL / /tmp path stays out of the terminal error" test is exactly the right regression to lock in.

Cross-checked the retry-counter contract against downloadWithRetry and runVideoExtractionWithRetry: both increment onRetry when a retry is scheduled — including the case where the retry attempt itself exhausts the budget (attempt=0 fails → increment → attempt=1 fails → throw). So phaseBreakdown.transientRetries reflects "retries scheduled and consumed," which matches the PR body claim.

Concerns (non-blocking; both same observability-vs-terminal-throw shape, safe to defer until enforce flips)

  • plan.ts throws failureToEnforce before extraction observability is emitted. packages/producer/src/services/distributed/plan.ts:920-929 calls runExtractVideosStage and immediately throws extractResult.failureToEnforce on the next line. Compare packages/producer/src/services/renderOrchestrator.ts:2147-2151: the in-process render path emits the full extractionObservability row (including transientRetries) FIRST, then throws. Once step 5 of your rollout flips candidate to enforce, the distributed planner (Lambda / GCP Cloud Run) will lose the per-failure telemetry that renderOrchestrator retains — exactly at the moment triage will need it. Cheap fix: emit the same observability checkpoint (or the subset that applies to the distributed planner) between the runExtractVideosStage return and the failureToEnforce throw, mirroring renderOrchestrator.ts:2147.
  • HDR-probe enforce failure bypasses the stage's own observability path too. In extractVideosStage.ts around the HDR probe (catch (error) block after runVideoExtractionWithRetry(() => extractMediaMetadata(...))), throw new VideoExtractionStageError(...) in enforce mode exits runExtractVideosStage before extractionResult.phaseBreakdown.transientRetries is written and before the caller's observability checkpoint fires. Same class of gap as above, but earlier in the stage. Cheap fix: catch the HDR probe error, stash a hdrProbeFailureToEnforce sibling of failureToEnforce, and let the same caller pattern that throws for extraction failures throw for HDR probe failures too — so a single call site owns the "emit observability, then throw" ordering.

Nits

  • assertVideoExtractionSucceeded is exported but appears test-only. The exported helper is used only by extractVideosStage.test.ts; production paths go through applyVideoExtractionFailurePolicyfailureToEnforce. If it's kept as a future public boundary (e.g. for the internal sidecar), a one-line docblock naming that intent would keep it from getting cargo-culted onto the enforce path.
  • runVideoExtractionWithRetry has two signal?.aborted checks per iteration (pre-op and post-error). Minor redundancy, but the post-error check preserves the classified diagnostic on the cancelled throw — arguably worth the duplication.

Question

  • Terminal-classifier registration timing. PR body says "internal sidecar and Experiment Framework must treat both exhausted stage codes as workflow-terminal … candidate enforcement must not be enabled until those companion mappings are deployed." Just want to make sure the AWS SAM NON_RETRYABLE_* + AWS CDK HyperframesRenderStack.ts + GCP Cloud Run NON_RETRYABLE_ERROR_NAMES also get updated (and per the PlanV2IntegrityError dual-naming pattern from the #2788/#2789/#2790 arc, both the class name "VideoExtractionStageError" AND the code strings "VIDEO_SOURCE_UNRENDERABLE" / "VIDEO_EXTRACTION_FAILED" land in those lists) — otherwise the AWS Step Functions retry classifier will match on neither and retry-storm through the full backoff. Assuming that's the plan for step 4/5; flagging so it doesn't slip.

What I didn't verify

  • Behavior when HF_VIDEO_EXTRACTION_FAILURE_MODE=enforce and a real render hits a mixed transient+deterministic failure set (I only read the retryable-summary logic in buildVideoExtractionStageErrorretryable = all failures are retryable === true, which correctly biases to VIDEO_SOURCE_UNRENDERABLE on any deterministic entry).
  • Cross-package audit that classifyVideoExtractionError's legacy string-matching branches ("[URLDownloader] Download timeout", "ffprobe deadline", etc.) match the exact strings still emitted at head after #2774 (the URL-downloader messages there use "Download timeout after ${timeoutMs / 1000}s" and "Download failed: ${message}" without the [URLDownloader] prefix — but that's fine because the UrlDownloadError typed branch fires first for downloader errors, so the string-match branch only matters for pre-migration legacy callers).

Review by Rames D Jusso

@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from 13e1704 to d0098b9 Compare July 26, 2026 17:58

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at d0098b9fdb66ccf4d594f34dd00f6415a0d2822b — verified pure rebase on #2774 R2 (07b607350e). HF#2776's own contribution slice (parent-head → tip) is patch-id-identical to the R1 slice (ce07556297400dd47ec7a0c1296518c027458ef6), so none of the child's own code changed.

Nothing to re-review at the code layer. My R1 findings all apply verbatim at the same file:line:

  • Non-blocking observability-vs-throw ordering: plan.ts:920-929 still throws extractResult.failureToEnforce before emitting extraction observability — mirror the renderOrchestrator.ts:2147-2151 "emit checkpoint, then throw" ordering before flipping candidate to enforce.
  • Non-blocking HDR-probe enforce-path gap in extractVideosStage.ts — HDR probe catch throws VideoExtractionStageError before phaseBreakdown.transientRetries is written, so the same "one call site owns the throw" fix closes both gaps.
  • Reminder: AWS SAM template.yaml, AWS CDK HyperframesRenderStack.ts NON_RETRYABLE_{PLAN,CHUNK,ASSEMBLE}, and GCP Cloud Run NON_RETRYABLE_ERROR_NAMES all need both the class name ("VideoExtractionStageError") AND the code strings ("VIDEO_SOURCE_UNRENDERABLE", "VIDEO_EXTRACTION_FAILED") added before step 5 of the rollout flips enforce — per the post-#2788 dual-naming pattern.

All still safe to defer while the flag defaults off. LGTM from my side, ready when its own CI settles and merge trigger comes.

Review by Rames D Jusso

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head d0098b9fdb66ccf4d594f34dd00f6415a0d2822b against parent #2774 head 07b607350e1fb6e722f94e47d161801b9ffceec3.

The default-off rollout shape, bounded 0|1 retry budget, safe kind/count-only producer error, and .code allowlist on the JSON/SSE boundary all look sound. Via and Rames already covered the enforce-lane observability and downstream terminal-classifier dependencies, so I am not repeating them.

One additive documented-standard blocker remains in new production code:

  • Important — new bare assertions violate CONTRIBUTING.md:47-54.

    • videoFrameExtractor.ts:201-202 checks the sentinel through (error as { hyperframesVideoSourceExtractionError?: unknown }).
    • server.ts:132 reads .code through (error as { code?: unknown }).
    • server.ts:134 then casts the narrowed string again to the literal union solely to satisfy Set.has.

    These are avoidable. The sentinel guard can use "hyperframesVideoSourceExtractionError" in error and compare the now-unknown property to true. The server helper can use the same object/non-null/"code" in error guard, then make the allowlist a ReadonlySet<string> (or use a typed predicate) so the already-validated string can be checked without a second assertion.

The two new test assertions at extractVideosStage.test.ts:196-197 repeat the same pattern after toBeInstanceOf(Error); convert those to an actual instanceof guard as well so the test's runtime proof narrows the type.

I attempted the focused suites in a clean worktree; repository package artifacts must be built in dependency order before Vitest can resolve the workspace exports, so I am relying on the exact-head CI matrix for the final execution signal rather than claiming those local attempts as test results.

Verdict: Request changes.
Reasoning: The feature architecture is safe while default-off, but the PR adds several bare production/test assertions contrary to the repository's explicit type-safety standard; all have small guard-based fixes.

— Magi

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two additional taxonomy defects remain at exact head d0098b9fdb66ccf4d594f34dd00f6415a0d2822b.

  • Blocker: Preliminary HDR probes run under Promise.all, and each enforce-mode catch immediately throws a one-member stage error (packages/producer/src/services/render/stages/extractVideosStage.ts:243-294). A transient rejection can win the race while another source has a deterministic failure, bypassing the aggregate “any non-retryable wins” classifier at extractVideosStage.ts:175-193. The engine's typed collection path demonstrates the aggregate seam (packages/engine/src/services/videoFrameExtractor.ts:1124-1154). Collect/all-settle preliminary failures, apply the aggregate classifier, and cover mixed transient-plus-deterministic ordering.
  • Blocker: FFmpeg launch maps only ENOENT to terminal unavailable; every other spawn error becomes retryable ffmpeg_transient (packages/engine/src/services/videoFrameExtractor.ts:616-635). Deterministic EACCES, ENOEXEC, and unknown launch failures therefore spend the candidate retry budget. Retry only known transient errnos and add errno-specific retry coverage.

Via and Rames already covered the workflow-terminal mapping dependency and HDR-probe telemetry timing, so I am not repeating those. My prior exact-head review separately covers the new bare type assertions. This child also depends on corrected #2774: merge the parent first, then rebase/refresh this PR and run the full exact-head CI matrix.

Verdict: Request changes.
Reasoning: Both paths can retry failures the declared contract treats as terminal, and the child cannot merge safely until corrected #2774 lands and a rebased full CI matrix completes.

— Magi

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Addressed all five requested type-safety assertions at cf37b42. Production now uses real object/property/string guards for the cross-realm sentinel and safe wire code; the tests use an instanceof guard before reading Error.message. No behavior or rollout changes. Revalidated 105 focused engine tests, 15 focused producer tests, both package typechecks, lint, format, tracked-artifact, and Fallow gates.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at cf37b42422c714eb84ef5ebff9b78f5df190fe00 — delta from d0098b9fdb is 11 lines across 3 files.

Assertion-standards delta is clean. All five findings fixed structurally, not by escape hatch:

  • videoFrameExtractor.ts:198-203isVideoSourceExtractionError sentinel check now uses "hyperframesVideoSourceExtractionError" in error && error.hyperframesVideoSourceExtractionError === true. Same semantics as the old as-cast read (both return undefined === true → false on a missing property), but no cast.
  • server.ts:118-121SAFE_RENDER_ERROR_CODES retyped Set<string> (dropped as const on the array literal). Enables the cast-free .has(code) call below. Very minor signature loosening: extractSafeRenderErrorCode now returns string | undefined rather than the narrower "VIDEO_SOURCE_UNRENDERABLE" | "VIDEO_EXTRACTION_FAILED" | undefined, but the runtime allowlist is identical, and no caller in the diff relies on the tighter union type.
  • server.ts:129-136extractSafeRenderErrorCode now reads error.code directly (safe because the preceding "code" in error guard already narrowed the type) and passes code to .has() without a cast. Same runtime allowlist, same terminal-message-scrubbing behavior. The three prior assertions the "signed URL / /tmp path stays out of the terminal error" regression pinned are all preserved.
  • extractVideosStage.test.ts:193-201 — replaced (caught as Error).message with if (!(caught instanceof Error)) throw new Error(...) then caught.message. Test now fails hard rather than passing through an as-cast; the two .not.toContain assertions run on a narrowed Error type.

Byte-audited that none of the substantive files from R1/R2 (plan.ts, extractVideosStage.ts runtime path, renderOrchestrator.ts) moved — my prior non-blocking findings still apply verbatim at the same file:line:

  • plan.ts:920-929 still throws extractResult.failureToEnforce before extraction observability emit; mirror renderOrchestrator.ts:2147-2151 ordering before enforcing.
  • extractVideosStage.ts HDR-probe catch still throws VideoExtractionStageError before phaseBreakdown.transientRetries is written.
  • SAM/CDK/GCP terminal-list registration reminder for both "VideoExtractionStageError" class name and "VIDEO_SOURCE_UNRENDERABLE"/"VIDEO_EXTRACTION_FAILED" code strings before step 5 rollout flips enforce.

All three still safe to defer while HF_VIDEO_EXTRACTION_FAILURE_MODE defaults off. LGTM from my side.

Review by Rames D Jusso

@jrusso1020
jrusso1020 force-pushed the fix/atomic-video-download-retry branch from 07b6073 to 4948966 Compare July 26, 2026 18:14
@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from cf37b42 to 36c4b63 Compare July 26, 2026 18:14

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial R2 exact-head review — hyperframes PR #2776

Head: cf37b42422c714eb84ef5ebff9b78f5df190fe00
Prior head: d0098b9fdb66ccf4d594f34dd00f6415a0d2822b
Delta: +11 / -11, 3 files (videoFrameExtractor.ts, server.ts, extractVideosStage.test.ts)


Verdict

COMMENTED — the Standards-lens fix I was asked to verify is structurally clean (all five bare as T / .message-without-guard findings from Magi's first CHANGES_REQUESTED are ADDRESSED at this head, no residual Standards violations introduced). However, Magi filed a second CHANGES_REQUESTED review at d0098b9fdb — separate from the Standards findings — flagging two taxonomy Blockers (HDR-probe Promise.all race + FFmpeg spawn-error errno mapping). The fix at cf37b42422 does not touch either surface; both blockers are NOT_ADDRESSED. Magi has not re-reviewed at cf37b42422 yet — both CHANGES_REQUESTED remain on record. My R1 seat is now neither blocking (my Standards findings are fixed) nor stamping (Magi's taxonomy blockers hold).


Miguel's findings verification

First CHANGES_REQUESTED review (Standards-lens, 2026-07-26T18:03:06Z @ d0098b9fdb)

Miguel enumerated the following at d0098b9fdb:

# Miguel's cite Fix at cf37b42422 Verdict
1 videoFrameExtractor.ts:201-202 — sentinel check uses (error as { hyperframesVideoSourceExtractionError?: unknown }) videoFrameExtractor.ts:201-202 — replaced with "hyperframesVideoSourceExtractionError" in error && error.hyperframesVideoSourceExtractionError === true (proper in guard, no cast) ADDRESSED
2 server.ts:132(error as { code?: unknown }).code server.ts:132 — reads error.code directly after the preceding "code" in error guard narrows the type. No cast. ADDRESSED
3 server.ts:134 — casts narrowed string to literal union to satisfy Set.has server.ts:121SAFE_RENDER_ERROR_CODES retyped new Set<string>([...]) (dropped as const); has(code) now called without a second cast at server.ts:133. Runtime allowlist identical. ADDRESSED
4 extractVideosStage.test.ts:196(caught as Error).message (assertion 1) extractVideosStage.test.ts:196-199if (!(caught instanceof Error)) throw new Error("expected VideoExtractionStageError") narrows caught before caught.message is read ADDRESSED
5 extractVideosStage.test.ts:197(caught as Error).message (assertion 2) Same instanceof guard covers both .message reads at lines 199-200 ADDRESSED

All five findings from Magi's first CHANGES_REQUESTED are structurally fixed (guards, not escape hatches). No as unknown as T, no ESLint disable, no test-only bypass.

Second CHANGES_REQUESTED review (taxonomy Blockers, 2026-07-26T18:05:30Z @ d0098b9fdb)

# Miguel's cite State at cf37b42422 Verdict
B1 extractVideosStage.ts:243-294 — HDR-probe Promise.all catch throws one-member VideoExtractionStageError synchronously; a transient rejection can win the race and preempt a deterministic failure elsewhere in the same batch, bypassing the aggregate "any non-retryable wins" classifier at extractVideosStage.ts:175-193. Body unchanged at cf37b42422 (lines 236-297 in this head). Enforce-mode catch still throws one-member VideoExtractionStageError(classified.retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE", ...) synchronously inside the Promise.all map. No allSettled / aggregate-classifier seam introduced. NOT_ADDRESSED
B2 videoFrameExtractor.ts:616-635 — FFmpeg spawn error maps only ENOENT → non-retryable ffmpeg_unavailable; every other spawn errno (EACCES, ENOEXEC, unknown) drops into retryable ffmpeg_transient, spending the candidate retry budget on deterministic failures. Body unchanged at cf37b42422 (lines 620-635 in this head). Still code === "ENOENT" → terminal, else → ffmpeg_transient retryable. No errno-specific coverage added. NOT_ADDRESSED

Magi's second review labels these "Blocker" not nit, and his second-review verdict remains "Request changes." Neither is touched by the fix diff. Both are gated by the default-off enforce lane + bounded 0|1 retry budget, so they're deferrable in the same class as my R1 Nit A / Nit B — but Magi has not yet dismissed them, so his CHANGES_REQUESTED holds.


Standards lens re-run on all three changed files at exact head cf37b42422

Bare as T (excluding as unknown as T and as const)

videoFrameExtractor.ts:121: comment prose — "classified as VFR and routed" (false positive; word "as" in English)
videoFrameExtractor.ts:621: (processResult.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT"
server.ts: (none)
extractVideosStage.test.ts:46: } as ExtractedFrames;  // in makeExtracted scaffold
  • videoFrameExtractor.ts:121 — comment; false positive.
  • videoFrameExtractor.ts:621 — bare as NodeJS.ErrnoException | undefined cast. Pre-existing (present at parent 07b607350e line 334, before this stack); confirmed via gh api /contents/…?ref=07b607350e grep. Not introduced by PR #2776's diff, so it's out of R2 scope as a Standards regression. However, this is the exact block Magi's Blocker B flags for errno-widening — so it lives at the intersection of a residual Standards fixture and an unaddressed taxonomy blocker. Any errno-widening fix should tighten this cast at the same time (e.g. if (typeof processResult.error === "object" && processResult.error !== null && "code" in processResult.error && typeof processResult.error.code === "string" && KNOWN_TERMINAL_ERRNOS.has(processResult.error.code))).
  • extractVideosStage.test.ts:46 — bare as ExtractedFrames scaffold. Pre-existing (present at parent 07b607350e line 35). Test fixture pattern that predates the stack; not a new violation for this PR. Would be worth cleaning up in a follow-up (build a proper factory or export a public constructor from @hyperframes/engine), but flagging it here only for completeness — Magi did not raise it, Rames did not raise it, and the PR did not touch this scaffold.

No new bare as T was introduced by the fix delta.

Non-null assertions (!., ![, !;)

videoFrameExtractor.ts: (none)
server.ts: (none)
extractVideosStage.test.ts: (none)

No non-null assertions in any of the three changed files.

.message reads in test file

extractVideosStage.test.ts:199-200 — expect(caught.message).not.toContain(...)

Both reads are preceded by the instanceof guard added at lines 196-198:

if (!(caught instanceof Error)) {
  throw new Error("expected VideoExtractionStageError");
}
expect(caught.message).not.toContain("/tmp/");
expect(caught.message).not.toContain("Signature");

The guard throws (not returns) so TS narrows caught for the remainder of the block. Correctly guarded.

Residual Standards findings summary

  • No new bare as T introduced by this fix.
  • One pre-existing bare as NodeJS.ErrnoException | undefined cast at videoFrameExtractor.ts:621 sits inside Magi's Blocker B range; the errno-widening fix should tighten it. Not a Standards regression from this PR — a follow-up hardening at most.
  • One pre-existing bare as ExtractedFrames scaffold at extractVideosStage.test.ts:46. Not introduced here — deferrable follow-up.

R1 findings status

Nit A — Step Functions / Cloud Run / Lambda handler don't list the new codes

State at cf37b42422: Unchanged. packages/aws-lambda/src/handler.ts:normalizeTerminalErrorName, the CDK Step Functions NON_RETRYABLE_PLAN list, and packages/gcp-cloud-run/src/server.ts NON_RETRYABLE_ERROR_NAMES still lack any entry for "VideoExtractionStageError", "VIDEO_SOURCE_UNRENDERABLE", or "VIDEO_EXTRACTION_FAILED". The PR body's rollout §4 still documents this as the gating dependency ("companion transport mappings" required before step 5 flips enforce). Not touched by the fix delta. Status: OPEN as a documented rollout dependency, not a landed defect. Same posture Rames re-affirmed in his cf37b42 re-review ("safe to defer while HF_VIDEO_EXTRACTION_FAILURE_MODE defaults off").

Nit B — HDR probe throws pre-checkpoint

State at cf37b42422: Unchanged. extractVideosStage.ts (production) is not in the fix delta; the enforce-mode catch inside Promise.all still throws VideoExtractionStageError synchronously before the extraction-stage telemetry checkpoint. This overlaps with (but is distinct from) Magi's Blocker B1: mine was about observability-row ordering; Magi's is about aggregate-classifier bypass under mixed transient+deterministic races. Both live in the same Promise.all block and would be addressed together by hoisting the throw to a post-Promise.allSettled aggregate seam. Status: OPEN, deferrable while enforce lane is off.

Multi-shape throw discipline: extractSafeRenderErrorCode still reads .code not .name?

Verified at cf37b42422: server.ts:130-134 — reads error.code (not error.name), allowlists against a Set<string> of the two canonical constants. Tests at server.errorCode.test.ts still assert .toBe("VIDEO_SOURCE_UNRENDERABLE") etc. — canonical string constants, not err.name / err.code. No trivial-pass smell. DISCRIMINATOR + TEST ASSERTIONS PRESERVED across the fix.


Peer state

Re-fetched /reviews at cf37b42:

  • vanceingalls (me) — APPROVED at d0098b9fdb (my R1). Stale relative to the new head, but not auto-dismissed.
  • james-russo-rames-d-jusso — three reviews on record (13e17046, d0098b9fdb, cf37b42422), all COMMENTED. The cf37b42422 re-review verifies the Standards fix structurally (all five findings addressed, no escape hatches) and re-affirms his prior deferrable findings — plan.ts:920-929 throws failureToEnforce before observability emit; extractVideosStage.ts HDR-probe throws pre-checkpoint; SAM/CDK/GCP terminal-list registration still pending for the two new codes + "VideoExtractionStageError". Explicitly labels them "safe to defer while HF_VIDEO_EXTRACTION_FAILURE_MODE defaults off."
  • miguel-heygen — TWO CHANGES_REQUESTED reviews at d0098b9fdb; has not re-reviewed at cf37b42422. Both blocking reviews remain on record. First (Standards) is now functionally resolved by the fix; second (taxonomy Blockers B1/B2) is untouched by the fix.

Inline review comments: gh api /pulls/2776/comments returns 0 — Magi's findings live in the review bodies, not as line-level comments. Verified.

PR-level issue comments: two — the Graphite stack bot, and James's fix-acknowledgement comment describing the exact scope of the cf37b42 delta.

Mergeability: mergeable_state: unstable — reflects Magi's still-open CHANGES_REQUESTED plus the parent-PR (#2774) dependency he called out.


Summary

The Standards-lens fix at cf37b42422 is clean by every measure I can grep: all five findings from Magi's first CHANGES_REQUESTED are structurally addressed (real in / instanceof guards, no cast escape hatches, Set<string> widened to drop the second cast), and the fix delta introduces zero new bare as T / non-null / unguarded .message reads. The two pre-existing bare casts elsewhere in the changed files (videoFrameExtractor.ts:621 errno path, extractVideosStage.test.ts:46 fixture scaffold) predate this PR — not regressions from this diff. However, Magi's second CHANGES_REQUESTED (HDR-probe Promise.all race + FFmpeg errno mapping — both labeled Blocker) is not addressed by this fix, and Magi has not re-reviewed at cf37b42422. My R1 approval stood on the Standards blindspot; that blindspot is now closed. From my seat, the Standards side is clean. From Magi's seat, the taxonomy Blockers still hold. My R2 verdict is COMMENTED — not blocking (my ask is met), not stamping (the merge cannot proceed while Magi's second CHANGES_REQUESTED sits, and the taxonomy Blockers are legitimate deferrable-only-under-default-off issues that need Magi's own dismissal or a follow-up fix).

— Review by Via

@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Restacked with Graphite directly on the final rebased #2774 parent. Exact review head is 36c4b6369457013a4b91904c2fb254e3115b7c57; focused extraction/error-code tests remain green (15/15) and producer typecheck passes. Please attach replacement approval to this head after fresh CI.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 36c4b63694 — verified pure rebase on new main (5bfda4e08737…).

git diff <merge-base>..<head> | git patch-id --stable returns 77b3ae1c2826939cad9b8de23f4c0fe86e5f1600 on BOTH the prior cf37b42422 and the new 36c4b63694 — so #2776's own contribution is byte-identical to R3. All R3 findings stand; the deferred substantive concerns from R2 (observability-before-throw in plan.ts:920-929, HDR probe path in extractVideosStage.ts, SAM/CDK/GCP dual-name registration before enforce flip) still apply verbatim at the same file:line, still safe to defer while default off.

Review by Rames D Jusso

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head re-review at 36c4b6369457013a4b91904c2fb254e3115b7c57.

The Graphite restack is patch-identical to the prior reviewed head. All five type-assertion findings are resolved with real narrowing, but the two independent failure-taxonomy blockers remain unchanged:

  • Blocker — concurrent HDR probe failures are race-classified. packages/producer/src/services/render/stages/extractVideosStage.ts:245-294 still throws a one-source VideoExtractionStageError from inside Promise.all. A transient rejection can win while another source has a deterministic failure, bypassing the later aggregate rule that any non-retryable failure must make the batch terminal. Collect outcomes first (allSettled or equivalent), then classify the full set; regression-test both completion orders with mixed transient/deterministic failures.
  • Blocker — deterministic FFmpeg launch failures are marked retryable. packages/engine/src/services/videoFrameExtractor.ts:620-635 still treats only ENOENT as terminal and maps every other spawn_error to retryable ffmpeg_transient. EACCES, ENOEXEC, and unknown launch failures cannot heal by retrying. Narrow the error shape honestly, whitelist only known-transient launch codes, and pin representative errno cases.

The child also remains dependent on corrected #2774. Via's current-head review independently records both blockers as not addressed; this review does not repeat the now-resolved standards findings.

Verdict: REQUEST CHANGES
Reasoning: The restack changed no PR-owned behavior, so mixed-source failure ordering can still produce the wrong retry class and deterministic FFmpeg spawn failures still consume the retry budget.

— Magi

@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from 36c4b63 to 54793b9 Compare July 26, 2026 18:34
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Addressed both current-head blockers and restacked on fixed #2774. Exact head: 54793b946eca033f689c2b1147ad7c5b0a7c14fd.

  • HDR video probes now collect all candidate-mode outcomes before classification. Enforce mode aggregates the full set, so any deterministic source failure makes the batch terminal regardless of completion order. Regressions cover transient/deterministic failures in both orders.
  • FFmpeg spawn classification now honestly narrows error.code, retries only known resource-pressure codes (EAGAIN, EMFILE, ENFILE), and makes ENOENT/EACCES/ENOEXEC/unknown launch failures terminal. Seven errno classification tests pin both sets.

Validation: producer focused tests 17/17, engine classification tests 7/7, both package typechecks, lint/format, and full pre-commit/Fallow pass.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 54793b946eca033f689c2b1147ad7c5b0a7c14fd — delta from R4 parent d4989f151a is #2776's own contribution: HDR probe aggregation + FFmpeg spawn errno whitelist + typed extraction failure taxonomy plumbing.

Both of my R2 substantive concerns are structurally addressed. The HDR probe path no longer preempts sibling probes with the first thrown error, and the FFmpeg spawn-error classifier now separates deterministic launch failures from transient resource-limit signals rather than letting either pattern retry indiscriminately.

HDR probe aggregation — R2 Concern 2 resolved. The Promise.all over composition.videos now maps each probe to null | { error, classified } instead of throwing eagerly. throwHdrProbeFailures(probeFailures.filter(isHdrProbeFailure), extractionPolicy.failureMode) runs after every probe completes:

  • In enforce: buildHdrProbeStageError(failures.map(f => f.classified)) builds a single aggregated VideoExtractionStageError with kind-count breakdown across ALL failed probes. Sorted by kind name for deterministic output. retryable = every failure is retryable. Correct taxonomy semantics — one deterministic failure in a batch of transient ones flips the whole batch to non-retryable.
  • In observe: throws firstFailure.error (the raw legacy exception) — matches the pre-migration surface so callers that catch on error message still work, but the aggregation is silently done (visible via log?.warn at the probe site).
  • In off: the try/catch re-throws immediately (legacy path); the aggregate branch never runs. Byte-for-byte compat.

FFmpeg spawn errno whitelist — the retry-storm concern I hadn't explicitly flagged but was latent. classifyFfmpegSpawnError now hard-codes:

  • ENOENTffmpeg_unavailable, non-retryable (missing binary — never fixes)
  • EAGAIN / EMFILE / ENFILEffmpeg_transient, retryable (POSIX resource-limit signals that clear on retry)
  • Everything else (including EACCES, ENOEXEC, unknown / no code) → ffmpeg_failed, non-retryable

The it.each test at videoFrameExtractor.errorClassification.test.ts pins both branches. The whitelist is narrow by design — an EPERM from a container sandbox won't retry (fail-closed), an EAGAIN from fork-exhaustion under load will. Right call: any errno I'd want to add to the transient set would need per-environment justification, and defaulting-narrow beats defaulting-wide when the alternative is retry-storm on real failures.

Typed extraction failure taxonomy — well-structured. VideoSourceExtractionError (thrown-form) + VideoExtractionFailure (collected-form) + VideoExtractionStageError (producer-boundary-form) form a three-layer surface:

  • Engine internals throw VideoSourceExtractionError with kind (14-member union: cancelled | source_missing | source_rejected | download_not_found | download_transient | invalid_media | media_start_out_of_range | ffmpeg_unavailable | ffmpeg_timeout | ffmpeg_transient | ffmpeg_failed | zero_output | internal | …) and retryable boolean.
  • ExtractionResult.errors now types as VideoExtractionFailure[] (kind + retryable optional for source-compat with older consumers, but always populated by this engine version).
  • Producer boundary throws VideoExtractionStageError with code (VIDEO_SOURCE_UNRENDERABLE | VIDEO_EXTRACTION_FAILED), retryable, and failures: [{kind, count}] — bounded message shape guaranteed not to contain paths or signed URLs (the assertVideoExtractionSucceeded test pins that no /tmp/ or Signature leaks). Message format: Video extraction failed for N source(s) [CODE; kind1=count,kind2=count] — deterministic, replayable, safe to forward.

The classifyVideoExtractionError mega-classifier covers UrlDownloadError (all 4 kinds), string-pattern legacy paths, HTTP status codes (404/410 → not_found, 408/429/5xx → transient), ffprobe deadline/absence, and fall-through to internal. All non-transient by default (fail-closed).

Bounded retry budget clamps boundedTransientRetryBudget(value: number | undefined): 0 | 1. Test fails closed to zero retries for a non-finite runtime retry budget locks the NaN case at 0 attempts — safe.

Producer error-code preservation via extractSafeRenderErrorCode. New SAFE_RENDER_ERROR_CODES Set<string> allowlist accepts only the two bounded producer codes; test does not forward arbitrary codes or parse message text pins that message-parsing is refused (even a message like "failed [VIDEO_SOURCE_UNRENDERABLE; secret=/tmp/x]" returns undefined). SSE and JSON error paths in server.ts:538-556 and :700-716 now forward the safe code when present. Sensible pattern.

Non-blocking observations (deferred until enforce flip)

  • R2 Concern 1 (plan.ts throws failureToEnforce before observability) is still the same shape. The new plan.ts:948-950 adds if (extractResult.failureToEnforce) throw extractResult.failureToEnforce; but there's no equivalent to renderOrchestrator.ts:2130-2151's observability checkpoint emit before it. Distributed planner (Lambda / GCP Cloud Run) will still lose per-failure telemetry that renderOrchestrator retains, once step 5 flips enforce. Cheap fix: mirror the perfSummary/observabilityCheckpoint emit shape between runExtractVideosStage return and the failureToEnforce throw.
  • hdrProbeTransientRetries is lost if HDR probe throws. The local counter only folds into extractionResult.phaseBreakdown.transientRetries at runExtractVideosStage line 419 — after throwHdrProbeFailures has potentially exited. In enforce mode + HDR probe failure, the operator sees the failure taxonomy but not the retry count from probes that succeeded before the failing one. Minor observability gap.
  • SAM/CDK/GCP terminal-list registration reminder still open. HyperframesRenderStack.ts NON_RETRYABLE_{PLAN,CHUNK,ASSEMBLE}, template.yaml ErrorEquals lists, and server.ts:900-922 NON_RETRYABLE_ERROR_NAMES need both the class name ("VideoExtractionStageError") AND the code strings ("VIDEO_SOURCE_UNRENDERABLE", "VIDEO_EXTRACTION_FAILED") before step 5 rollout flips enforce — per the post-#2788 dual-naming pattern (both error.name-shape and error.code-shape throws register terminally).

All three still safe to defer while HF_VIDEO_EXTRACTION_FAILURE_MODE defaults off.

LGTM from my side.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial R3 exact-head review — hyperframes PR #2776

Head: 54793b946eca033f689c2b1147ad7c5b0a7c14fd
Prior head: 36c4b6369457013a4b91904c2fb254e3115b7c57

Both Miguel-cited blockers structurally addressed. Standards lens re-run clean over the fix delta at head.

B1 — Concurrent HDR probe failures — ADDRESSED

packages/producer/src/services/render/stages/extractVideosStage.ts:

  • Per-source catch (error) (.ts:314-324) now returns { error, classified } for observe/enforce, only re-throws when failureMode === "off". Promise.all cannot short-circuit on the first probe rejection in candidate mode — this is behaviorally identical to allSettled for that path.
  • Post-settle aggregation probeFailures.filter(isHdrProbeFailure) produces the full set (.ts:284-327).
  • throwHdrProbeFailures (.ts:223-233) → buildHdrProbeStageError (.ts:196-212) uses failures.every((failure) => failure.retryable) to pick the code. Any non-retryable failure in the set flips the whole batch to terminal VIDEO_SOURCE_UNRENDERABLE regardless of completion order.
  • Order-independent regression at extractVideosStage.test.ts:249-269: it.each runs both orderings (download_transient first vs. source_missing first), both assert code: "VIDEO_SOURCE_UNRENDERABLE", retryable: false.

B2 — FFmpeg spawn errno mapping — ADDRESSED

packages/engine/src/services/videoFrameExtractor.ts:686-711:

  • TRANSIENT_FFMPEG_SPAWN_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE"]). Explicit allow-list.
  • ENOENTffmpeg_unavailable (terminal). Every other value of error.codeEACCES, ENOEXEC, EPERM, missing/empty, or any unknown errno — falls through to ffmpeg_failed with retryable: false. Default-deny, the inverse of the prior default-allow shape.
  • Regression at videoFrameExtractor.errorClassification.test.ts:1-20 pins ENOENT/EACCES/ENOEXEC/UNKNOWN as terminal and EAGAIN/EMFILE/ENFILE as retryable. EPERM is covered structurally by the UNKNOWN case but not named; not a blocker.

Standards lens re-run

Grepped bare as T (excl. as const / as unknown as T), non-null !. / ![ / !;, angle-bracket casts, and .message reads across every file in the fix delta at 54793b946e. Newly added code in the B1/B2 slice: zero bare as T, zero non-null assertions, zero angle-bracket casts, zero unguarded .message. All hits (renderOrchestrator.ts DOM element casts, plan.ts:615 data-shape helper, extractVideosStage.ts:365 bounded-index !, extractVideosStage.test.ts:47 and videoFrameExtractor.test.ts:1672 fixture } as ExtractedFrames) are pre-existing and untouched by the fix commits — verified against the + slice of the diff. caught.message in extractVideosStage.test.ts:200-201 is inside a if (!(caught instanceof Error)) throw guard; all error.message reads in server.ts, renderOrchestrator.ts, plan.ts, urlDownloader.ts, and videoFrameExtractor.ts are instanceof Error-guarded.

R1 Nit A

Still deferred as documented rollout dependency. Step Functions NON_RETRYABLE_PLAN/_CHUNK/_ASSEMBLE at packages/producer/src/services/distributed/aws-stack.ts:196-239 still don't include VIDEO_SOURCE_UNRENDERABLE/VIDEO_EXTRACTION_FAILED — anticipated at R1 and R2. SAFE_RENDER_ERROR_CODES in server.ts:121-124 does include both codes, so the HTTP/SSE boundary is complete. Not blocking.

Peer state

Miguel has not re-reviewed at 54793b946e yet; his most recent review at 36c4b63694 is CHANGES_REQUESTED. James posted an issue comment at 18:34:59Z on 2026-07-26 summarizing the fixes and validation (producer focused tests 17/17, engine classification 7/7, both package typechecks, lint/format, full pre-commit/Fallow pass) — consistent with what I verified in code. My APPROVE stands independently.

Verdict: APPROVE
Reasoning: Both blockers structurally addressed with order-independent regression coverage; default-deny errno allow-list is the correct inversion; standards delta clean; deferred R1 Nit A is out of scope by design.

— Review by Via

@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from 54793b9 to 6265516 Compare July 26, 2026 18:42
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Graphite-restacked on the final #2774 boundary fix. Exact child head is now 6265516ee4d4c33ba045f05636412a84f3754618; the #2776 patch is unchanged.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head re-review at 6265516ee4d4c33ba045f05636412a84f3754618.

Both prior code blockers are resolved cleanly:

  • extractVideosStage.ts:284-327 now lets every candidate-mode HDR probe settle into a typed outcome before classifying the batch. buildHdrProbeStageError uses failures.every(...), so any deterministic source failure makes the aggregate terminal regardless of completion order. The two-order regression at extractVideosStage.test.ts:249-268 pins that invariant.
  • videoFrameExtractor.ts:686-710 now retries only the explicit resource-pressure errno allowlist (EAGAIN, EMFILE, ENFILE). ENOENT remains ffmpeg_unavailable; EACCES, ENOEXEC, missing, and unknown codes fail closed as terminal ffmpeg_failed. The seven-case classification table pins both sides.

The narrowed error reads and new tests are standards-clean. Focused verification of the patch-identical child slice passes 24/24 under Vitest.

This is a pure Graphite restack of the reviewed child slice onto corrected #2774 8dd0c68c68: the stable patch ID is identical (4e7f917e…), and the only whole-head delta from 54793b946e is the parent's three-line 240.0.0.0/4 fix plus regression. The parent probe now blocks both 240.0.0.1 and 255.255.255.255.

Verdict: COMMENT
Reasoning: Both implementation blockers and the parent dependency are resolved. This head is code-clear; I am holding the replacement approval only until its fresh required CI is terminal green.

— Magi

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 6265516ee4d4c33ba045f05636412a84f3754618 — verified pure rebase on new #2774 head (8dd0c68c68).

git diff <#2774-head>..<#2776-head> | git patch-id --stable returns 4e7f917e6f8221fc4f5bc5fbe7110469f8b3a117 on BOTH the prior 54793b946e and the new 6265516ee4 — so #2776's own contribution is byte-identical to R5. All R5 findings stand, including the three deferred rollout-blockers (plan.ts observability-before-throw, probe-retry counter loss on failure, SAM/CDK/GCP dual-naming registration) still safe to defer while default off.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte-clean re-stamp at 6265516ee4. Verified blob-SHA equivalence against prior R3 head 54793b946e:

  • videoFrameExtractor.ts — MATCH (blob a3d7df8699…)
  • extractVideosStage.ts — MATCH (blob 5b0248be61…)
  • server.ts — MATCH (blob 9a3a2f88c3…)
  • urlDownloader.ts — DIFFERS. This is expected — #2776 is stacked on #2774, and the Graphite restack propagates #2774's Class-E 240.0.0.0/4 fix into the child's copy of urlDownloader.ts. Verified by diffing the two blobs: the only delta is the single ["240.0.0.0", 4] subnet addition — same content #2774 R4 verified at 8dd0c68c68.

Prior R3 APPROVE at 54793b946e stands for #2776's own scope (HDR-probe aggregate + errno allow-list). No new #2776-scoped changes introduced by the rebase.

Review by Via

@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from 6265516 to c0690c6 Compare July 26, 2026 19:01
@jrusso1020
jrusso1020 force-pushed the fix/atomic-video-download-retry branch from 8dd0c68 to 2e84fae Compare July 26, 2026 19:01

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at c0690c60e2 — verified pure rebase on merged-main (a8ee81f3d8…, containing #2773).

git diff <merge-base>..<head> | git patch-id --stable returns 38a95a12e05db564dac33e1033765373dc52fb0c on BOTH 6265516ee4 and c0690c60e2#2776's own contribution byte-identical to R6. R6 findings stand; three deferred rollout-blockers (plan.ts observability-before-throw, probe-retry counter loss on failure, SAM/CDK/GCP dual-naming registration) still safe to defer while default off. Noted James will rebase again on merged-main after #2774 lands before the final stamp.

Review by Rames D Jusso

vanceingalls
vanceingalls previously approved these changes Jul 26, 2026

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte-clean re-stamp at c0690c60e2. Pure Graphite rebase after #2773 merged to main.

Verified blob-SHA equivalence on all 12 PR-scoped files vs prior R3 head 6265516ee4:

  • 11 files MATCH by blob-SHA (urlDownloader.ts, urlDownloader.test.ts, videoFrameExtractor.ts, videoFrameExtractor.test.ts, videoFrameExtractor.errorClassification.test.ts, extractVideosStage.ts, extractVideosStage.test.ts, server.ts, server.errorCode.test.ts, observability.ts, renderOrchestrator.ts, engine/src/index.ts)
  • plan.ts DIFFERS — expected. #2773 merged and modified plan.ts extensively (added planSize integration + .plan-work scrub + PlanTooLargeError breakdown). #2776's own delta on plan.ts remains the same single +1 line at plan.ts:999: if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;. Verified by inspecting the patch at c0690c6#2776's own hunk is unchanged.

No content drift on #2776's own scope; rebase is pure post-#2773 main catch-up on plan.ts, plus #2776's original +1 line preserved. Prior R3 APPROVE stands. Miguel's two Blockers (HDR-probe race B1, FFmpeg errno allow-list B2) are still cleanly addressed at this content.

Review by Via

miguel-heygen
miguel-heygen previously approved these changes Jul 26, 2026

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head approval at c0690c60e2549371adb69e2041f24e3ff1af0d98.

Both prior blockers remain resolved: candidate-mode HDR probes settle before aggregate classification, and FFmpeg launch retries use a narrow resource-pressure errno allowlist with deterministic failures terminal. Focused verification of this slice passes 24/24.

This Graphite head moved only with its corrected parent after #2773 merged. The #2776 child stable patch ID is unchanged from the reviewed slice (4e7f917e…), and its exact base is current #2774 2e84faeb28.

Verdict: APPROVE
Reasoning: The blocker fixes are regression-locked, the child contribution is patch-identical after the dependency restack, and no new #2776 behavior entered the exact head.

— Magi

@jrusso1020
jrusso1020 changed the base branch from fix/atomic-video-download-retry to graphite-base/2776 July 26, 2026 19:45
@jrusso1020
jrusso1020 force-pushed the fix/video-extraction-failure-retry branch from c0690c6 to 33ca1de Compare July 26, 2026 19:48
@jrusso1020
jrusso1020 changed the base branch from graphite-base/2776 to main July 26, 2026 19:48
@jrusso1020
jrusso1020 dismissed stale reviews from vanceingalls and miguel-heygen July 26, 2026 19:48

The base branch was changed.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 33ca1de0631be66bfa5c591a231256c69667226e — verified pure rebase on merged-main (814f9cd977…, containing #2774).

git diff <merge-base>..<head> | git patch-id --stable returns 4e7f917e6f8221fc4f5bc5fbe7110469f8b3a117 on BOTH the prior c0690c60e2 and the new 33ca1de063#2776's own contribution byte-identical to R7. R7 findings stand; three deferred rollout-blockers (plan.ts observability-before-throw, probe-retry counter loss on failure, SAM/CDK/GCP dual-naming registration) still safe to defer while default off.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte-clean re-stamp at 33ca1de063. Pure post-#2774-merge Graphite rebase.

Verified blob-SHA equivalence across all 13 PR-scoped files vs prior head c0690c60e2 — 13/13 MATCH:

  • All engine + producer files (urlDownloader.*, videoFrameExtractor.*, videoFrameExtractor.errorClassification.test.ts, extractVideosStage.*, server.*, plan.ts +1 hunk, observability.ts, renderOrchestrator.ts, engine/src/index.ts) unchanged.

Rebase is genuinely patch-identical — no content drift on #2776's own scope, and the parent-merge propagation of #2774 is now fully absorbed into main.

Prior R3 APPROVE + prior byte-clean re-stamp both stand. Miguel's Blockers B1 (HDR-probe aggregate) + B2 (FFmpeg errno allow-list) remain cleanly addressed at this content.

Review by Via

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head re-stamp at 33ca1de0631be66bfa5c591a231256c69667226e.

I independently verified this is a pure post-#2774-merge rebase of the already-reviewed #2776 child slice:

  • Stable patch ID is identical before and after the rebase: 4e7f917e6f8221fc4f5bc5fbe7110469f8b3a117.
  • The prior and current child diffs contain the same 13 files.
  • All 13 PR-scoped files match the prior reviewed head c0690c60e2 by blob SHA.
  • The exact-head required matrix is terminal green, including Build, Test, Typecheck, runtime contract, and both Windows jobs.

The previously resolved blockers remain regression-locked: candidate-mode HDR probes settle before aggregate classification, and FFmpeg launch retries use the narrow resource-pressure errno allowlist with deterministic launch failures terminal.

Verdict: APPROVE

Reasoning: No child-slice content drift occurred during the final restack, and every required exact-head check is green.

— Magi

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R8 (pure-rebase-on-merged-#2774; patch-id 4e7f917e… identical to R7 slice) stands. Fresh CI settled clean. Applying stamp per James's ask at 1785095419.012049.

jrusso1020 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Jul 26, 8:32 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 26, 8:33 PM UTC: @jrusso1020 merged this pull request with Graphite.

@jrusso1020
jrusso1020 merged commit f500a42 into main Jul 26, 2026
48 of 54 checks passed
@jrusso1020
jrusso1020 deleted the fix/video-extraction-failure-retry branch July 26, 2026 20:33
dahans-msft2 pushed a commit to dahans-msft2/hyperframes that referenced this pull request Aug 6, 2026
## Summary
- classify per-source video download/probe/decode/extraction failures with a bounded taxonomy and safe producer-facing summaries
- add candidate-only, at-most-one transient retry with cleanup and retry telemetry
- preserve default engine/producer behavior when the policy is off
- carry allowlisted extraction error codes through blocking JSON and SSE responses

## Stack
Depends on heygen-com#2774 for atomic remote downloads and its single owned download retry. This PR is intentionally based on `fix/atomic-video-download-retry`; rebase/change the base to `main` after heygen-com#2774 merges.

## Default compatibility
`HF_VIDEO_EXTRACTION_FAILURE_MODE` defaults to `off` and forces `maxTransientRetries=0`.

With the feature off:
- metadata probe failures keep the legacy Promise rejection
- grouped extraction keeps the existing grouped-to-direct fallback
- no new producer failure gate is enforced
- render-plan schema, Plan v1 artifacts, chunk routing, and distributed execution are unchanged

Typed metadata aggregation is explicit and enabled only by the candidate enforce lane.

## Retry ownership
- remote downloads: exactly one retry owned by heygen-com#2774
- metadata/FFmpeg extraction: at most one retry only when `HF_VIDEO_EXTRACTION_MAX_RETRIES=1`
- invalid, missing, rejected, out-of-range, zero-output, cancellation, and unknown/internal failures do not retry
- non-finite or invalid runtime retry budgets fail closed to zero
- the superset optimization is never retried; on failure it preserves direct-member fallback, and only the individual ranges can use the bounded retry
- retry counters increment when a retry is scheduled, including exhausted retries

The internal sidecar and Experiment Framework must treat both exhausted stage codes as workflow-terminal after the producer-local budget. Candidate enforcement must not be enabled until those companion mappings are deployed, or Temporal can multiply producer attempts.

## Failure contract
- `VIDEO_SOURCE_UNRENDERABLE`: at least one deterministic/unknown source failure
- `VIDEO_EXTRACTION_FAILED`: all source failures are transient but the producer-local budget is exhausted

Only the allowlisted code and kind/count summaries cross JSON/SSE. Raw diagnostics remain engine-local because they may contain signed URLs or local paths.

## Rollout
1. merge and deploy with stable/candidate both `off`
2. candidate `observe`, retries 0
3. candidate `observe`, retries 1
4. deploy internal + EF terminal transport mappings
5. candidate `enforce`, retries 1
6. keep stable off until success delta, retry counts, extraction latency, CPU/disk, and queue backlog are acceptable

## Validation
- engine focused suites: 105 passed
- producer focused suites: 15 passed
- full engine suite: 1,176 passed, 3 skipped
- full producer unit lane: 32 Vitest files / 393 tests plus all classified Bun unit tests
- engine and producer typechecks passed
- oxlint, oxfmt, Fallow, tracked-artifact, and commit hooks passed
- independent review: approved for merge default-off; candidate enforcement held on companion transport rollout
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants