Skip to content

fix(core): a composition behind a wrapper takes its host's start - #6435

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/wrapped-mount-host-start
Oct 26, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/wrapped-mount-host-start

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What

A composition root with no data-start of its own now takes its start from the nearest composition host or timed wrapper above it, even when a neutral element (a <section>, a vfx <canvas>, an author's wrapper div) sits between them. A wrapper with its own data-start wins over the host above it, because that wrapper is what shows and hides the composition. Before, only the immediate parent counted, so a wrapped composition was timed on the root clock: it started at 0 instead of its host's start, and once root time passed its own duration its layers went hidden.

Why

#4334. With a host at 5 s and one neutral wrapper between it and the inner root, the resolver returned 0 for every host marker (data-composition-src, -id, -file); with no wrapper it returned 5.

Related work

Fixes #4334. Follows the maintainer's call on the issue: keep the resolver marker-only (no special case for canvas.hf-vfx-src); the exporter keeps copying data-start onto its wrapped element.

How

inheritedTimingHost in packages/core/src/runtime/startResolver.ts now walks to the nearest ancestor carrying a composition marker or a data-start (closest()) instead of checking only the parent. A timed wrapper counts because the runtime hides it outside its window: timing the composition from the host would start its clock early and show it mid-animation. CSS animations already pick their clock the same way (closest("[data-start]") in the CSS adapter). It stops at the first match, so an outer host or a sibling clock is never used, and explicit starts, references and fallbacks are untouched. isCompositionHost uses the same selector, so the marker list has one definition.

Test plan

  • startResolver.test.ts: the full 3x3 matrix from the issue (each marker, 0/1/2 neutral wrappers) plus a nested-host case that must take the nearest host (3 s inside a host at 2 s = 5 s), plus a timed wrapper at 2 s inside a scene at 5 s that must give 7 s. 45 passed, 3 runs. With the old parent-only lookup restored, exactly the 7 new wrapper/nested cases fail; with a host-only closest(), the timed-wrapper case fails (expected 5 to be 7).
  • Real render, wrappedMountTiming.integration.test.ts (Chrome + ffmpeg): a scene at 1 s holds, inside a <section>, a composition whose box fades in over its own first second. Halfway through the scene the frame is half grey (between 90 and 165 of 255), not fully faded in. 3 runs green; with the old lookup it fails (expected 255 to be less than 165).
  • src/runtime in core: 104 files passed.
  • oxfmt, both tsc --noEmit (core, producer), comment ratchet, producer test classification: clean.
  • Comments follow CONTRIBUTING.md "Comments".

Why under 100 lines of source: the bug is one lookup in the one resolver every timing path goes through; the rest of the diff is the matrix and the render test.

@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.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head db8ee7ab

  • blocker — required Test reachability fails: Runner mapping needs review: packages/producer/scripts/test-classification.mjs. Editing that file changes its digest, and scripts/test-reachability.json:50 still pins the old one (88d7415a…). Re-pin it to 47a6204722443284067bf5c464601a252091aab3670ef1626e50ce507b8214fc, as #4610 did. With that one-line change, node scripts/check-test-reachability.mjs --report passes locally.

I found nothing wrong in the resolver change. A wrapped root now resolves the way a directly mounted one already did, and timeline.ts already used the nearest composition's start as its fallback.

Scope

Read all 4 files and the callers of resolveStartForElement, resolveHostSlotsForElement and resolveCompositionOrigin. Locally: startResolver.test.ts 44/44, and 7 fail with the old lookup restored. Core src/runtime passes 2557/2557. wrappedMountTiming.integration.test.ts passes with Chrome and ffmpeg. The other CI jobs were still running when I posted this.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 188 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 (0)

The nearest host or timed wrapper above a mounted root sets its start, so its clock starts when the wrapper shows it, matching how CSS animations already pick their clip.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Re-pinned the classification digest in scripts/test-reachability.json (a66cd1f); node scripts/check-test-reachability.mjs passes, and fails with the old pin. a66cd1f also has 81a4848: a composition inside a wrapper with its own data-start now starts with that wrapper, not the host above it, since the wrapper is what shows and hides it (test: wrapper at 2 s in a scene at 5 s gives 7 s).

@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.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · db8ee7a → a66cd1f. The test-reachability.json digest is re-pinned, and I read the timed-wrapper change in 81a4848 and found no blocker. Locally: reachability passes, core src/runtime 2558/2558. Required CI is green.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 0979e32 Oct 26, 2026
82 checks passed
@miguel-heygen
miguel-heygen deleted the fix/wrapped-mount-host-start branch October 26, 2026 17:12
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 26, 2026
Merge 20 more upstream commits (acd0bc3..ad4d3fa, v0.8.205: heygen-com#6403,
heygen-com#6363, heygen-com#6209, heygen-com#6395, heygen-com#6424, heygen-com#6426, heygen-com#6442, heygen-com#6430, heygen-com#6402, heygen-com#6435, heygen-com#6439,
heygen-com#6449, heygen-com#5436, heygen-com#6455 and docs). No conflicts. Record the 11 open PRs
heygen-com#6440-heygen-com#6458 and issue heygen-com#6443 in docs/DECISIONS.md (none adopted), re-check
the earlier open PRs, and advance the commit watermark to ad4d3fa, the
PR watermark to heygen-com#6458 and the issue watermark to heygen-com#6443.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

runtime: startResolver only checks the immediate parent for composition attributes, so a mount wrapped by any element is seeked on the root clock

1 participant