Skip to content

fix(producer): a drive that refuses frame links gets a copy, whatever code it refuses with - #7163

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/frame-link-fallback
Nov 4, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/frame-link-fallback

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

What

When a drive refuses the link that puts a render's extracted frames into its work folder, the render now falls back to a junction and then a copy, whatever error code the drive refuses with. Three failures copying cannot fix still propagate: a full disk (ENOSPC, EDQUOT), an entry already at the destination (EEXIST, which stageExtractedFrameDir clears and relinks, e.g. a dangling link left after the extraction cache was collected), and a missing source (ENOENT).

Why

linkOrCopyFrameDir fell back only when the symlink failed with EPERM, EACCES or UNKNOWN, and tryWindowsFrameJunction only for a fixed list of codes; any other code was rethrown and the render failed. A Windows desktop user exporting to an external drive (E:\…) had the render fail at once on symlink '<frame cache>' -> '<work folder>' with a code outside that list, while the same render would have worked with the copy the fallback already does. A drive can refuse links in more ways than the lists named (FAT32 and exFAT drives cannot hold links at all), so the lists were the wrong shape: every refusal is a reason to copy, except the few failures a copy cannot fix.

Related work

#3740 added the junction before the copy and its rule that a disk failure is never hidden by copying; that rule stays and now covers the symlink attempt too.

How

  • packages/producer/src/services/render/shared.ts: one copyCannotFix check (ENOSPC, EDQUOT, EEXIST, ENOENT). The symlink attempt rethrows only those, then tries the junction; the junction attempt rethrows only those, then returns false so the existing copy runs. EEXIST keeps reaching stageExtractedFrameDir's self-heal (review). The comment above linkOrCopyFrameDir now says this, at the same line count. The once-only "copying instead" log line is unchanged.

fails without the fix: packages/producer/src/services/windowsFrameStaging.test.ts › Windows link fallbacks › copies after the drive refuses the symlink with EISDIR and the junction too

Size

Small on purpose: two allow-lists become one check of what a copy cannot fix, at the one place frame folders are linked, with its tests.

Test plan

  • windowsFrameStaging.test.ts, new: a symlink and junction refused with EISDIR, EIO or EINVAL end in a copy with the frames readable; a symlink that finds the disk full fails the render with no junction and no copy. A real dangling dir link at the destination ends as a link to the frames (real filesystem, not on Windows). The existing cases still pass, including "does not hide a disk failure from the junction attempt". On Linux at this commit: 15 passed, 1 skipped (the real-Windows case), 3 runs in a row. With main's shared.ts, the three new fallback cases fail; with the first head's (1e8c7768), the dangling-link case fails.
  • oxfmt --check, oxlint on both files: clean. check-comment-citations and comment-ratchet against main: OK. tsc --noEmit in packages/producer: no errors in the changed files.
  • A real FAT32 or exFAT drive on Windows: not run. Which code Windows gives for a link there is not checked here; the change makes the code not matter.
  • Comments follow CONTRIBUTING.md "Comments".

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes at 1e8c7768. Widening the fallback swallows EEXIST, and that breaks the stale-link self-heal in stageExtractedFrameDir.

Blocker: a dangling frame link left by a GC'd extraction cache now fails the render

stageExtractedFrameDir relies on EEXIST reaching it from the symlink attempt. Its handler removes the stale entry and stages again. The comment there describes the case: a dangling symlink at dest left after the extraction cache was GC'd, or a dir reused from a Linux run.

Before this PR, linkOrCopyFrameDir rethrew EEXIST, because it wasn't in the allow-list. Now every non-full-disk code goes on to the fallback:

  • On POSIX, the junction is skipped and cpSync(src, dest) runs onto the existing link.
  • On Windows, the junction also gets EEXIST, returns false, and the same cpSync runs.

cpSync throws ERR_FS_CP_DIR_TO_NON_DIR, not EEXIST, so the self-heal never runs and the render fails.

I reproduced it on Linux with a real filesystem. I put a dangling dir symlink at compiled/__hyperframes_video_frames/clip, then called materializeExtractedFramesForCompiledDir:

  • main: passes, and clip is relinked as a symlink.
  • this head: throws ERR_FS_CP_DIR_TO_NON_DIR: Cannot overwrite non-directory …/clip with directory …/cache.

Suggested fix: let EEXIST through as well as a full disk, in the symlink catch and in tryWindowsFrameJunction. For example, if (isDiskFull(err) || code === "EEXIST") throw err;. Then add a test with a real dangling link at dest that ends as a link to the frames. That test fails at this head.

Non-blocking

  • The comment above linkOrCopyFrameDir still says it falls back only on "Windows' no-symlink-privilege errors (EPERM/EACCES, plus UNKNOWN)" and that "Other symlink errors … propagate". Both are false now. Editing it in place keeps the comment-line count the same.
  • ENOENT (a missing src) now goes through the junction and copy before it fails, and the message comes from cpSync. That's harmless, but the "copying instead" log now names ENOENT.

Checked

  • The fails without the fix: line names a test that exists. windowsFrameStaging.test.ts passes at this head: 14 passed, 1 skipped.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 117 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Right, thank you: that broke the stale-link self-heal. Fixed at 13dbb39a: EEXIST propagates again from the symlink and junction attempts, so stageExtractedFrameDir clears the stale entry and relinks. The check is now copyCannotFix (ENOSPC, EDQUOT, EEXIST, ENOENT); ENOENT covers your log note, a missing source fails with its own code. New test with a real dangling dir link at compiled/__hyperframes_video_frames/clip: it ends as a link to the frames; it fails at 1e8c7768 and passes here. The comment above linkOrCopyFrameDir is corrected in place (same line count). Staging file: 15 passed, 1 skipped, 3 runs.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at 13dbb39a. The blocker from my review at 1e8c7768 is fixed.

What I ran on this head

  • copyCannotFix now lets EEXIST and ENOENT through as well as a full disk, in both the symlink catch and tryWindowsFrameJunction. So a stale entry at dest reaches the self-heal in stageExtractedFrameDir again.
  • On Linux, windowsFrameStaging.test.ts passes at this head: 15 passed, 1 skipped.
  • With the previous head's shared.ts, only the new "relinks over a dangling frame link left where the frames go" test fails.
  • With main's shared.ts, only the three fallback cases fail, including the test named on the fails without the fix: line. That matches my earlier reproduction.
  • The comment above linkOrCopyFrameDir now matches the code. Comments, PR captures, preview-regression and Player perf are green, and one CI run is green on this head. A second CI run, Windows render, regression and CodeQL were still running when I approved.

Non-blocking

  • The dangling-link test asserts that dest is a symlink and reads the frame through extracted.framePaths (the source path). A dangling link is also a symlink, so the test proves the re-link mainly because nothing throws. Reading the frame through dest, or checking readlinkSync(dest), would pin the result.
  • ENOENT propagating matches main's behaviour, so nothing changes there.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Taken at 5b04fd6e: the dangling-link test now reads the frame through dest (join(dest, "frame_000001.jpg")), so a link that stayed dangling fails it. Test only; 15 passed, 1 skipped, 3 runs; fails with the first head's shared.ts.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at 5b04fd6e. The only change since my approval at 13dbb39a is one line in windowsFrameStaging.test.ts. The dangling-link test now reads the frame through dest (join(dest, "frame_000001.jpg")). It no longer reads the source path, so the test now checks that the link really points at the frames. That was my non-blocking note.

shared.ts is unchanged. The test file passes on this head (15 passed, 1 skipped). CI, CodeQL, Comments, PR captures, preview-regression and Player perf are green; Windows render and regression were still running when I approved.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 4, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit 845bd63 Nov 4, 2026
91 checks passed
@miguel-heygen
miguel-heygen deleted the fix/frame-link-fallback branch November 4, 2026 16:24
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 5, 2026
Ports: heygen-com#7158 (failed run exits 1 when its output pipe closes early),
heygen-com#7161 (ENOSPC during frame extraction reports disk_full), heygen-com#7163 (copy
frames when a drive refuses links), heygen-com#7171 (retry with fewer workers when
every page times out), heygen-com#7172 (split a clip whose start names another
clip, adapted), heygen-com#7169 (lint rule audio_attribute_unreadable), and early
heygen-com#7179, heygen-com#7173. Watermarks: commit 370f886, PR 7164; monitored 91.

Co-authored-by: Miguel Ángel <[email protected]>
Co-authored-by: WaterrrForever <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.

1 participant