- Portrait compositions render the full frame when the project was
scaffolded at a different size: html/body are sized to the composition
root at runtime; new lint warning root_dimensions_mismatch (#4005,
fixes#4001)
- render_complete telemetry adds composition_element_tags,
aroll_video_count, heygen_video_count; render_error adds error_name and
failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
search no longer shows [object Object] (#4006)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* feat(telemetry): add per-tag element and a-roll counts to render_complete
countElementTags's single regex scan (the ~83% of renders that get no
live probe session) is extended into scanElementTags, which returns a
per-tag breakdown and an a-roll video count alongside the same total,
all from one pass over the same script/style-stripped markup — the
total is the sum of the per-tag map by construction, so the two
properties can't drift apart. countElementTags itself is removed; it
had zero production callers after this change.
New render_complete properties: composition_element_tags ({tag: count},
raw lowercase tag names, capped at the top 50 by count with the rest
folded into an "other" key) and aroll_video_count (<video
data-aroll="true"> elements from the same scan). Both are static-scan-
only, mirroring the existing composition_element_count_source split —
the live probe-DOM path never runs this scan.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* feat(telemetry): count HeyGen-provenance video from the same tag scan
heygenVideoCount joins arollVideoCount/byTag as a third property
derived from scanElementTags's single pass: <video
data-media-source="heygen"> elements. media-use's resolve.md now
documents that the skill stamps this attribute on a mounted video only
when the ledger record's provider is "heygen.video" (the mount itself
is always the agent's own composition edit, never written by resolve
scripts) — the ledger stays the single source of truth for provenance,
the HTML attribute is a projection of it, and no other provider value
is invented beyond this one signal.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* feat(telemetry): add a bucketable failure taxonomy to render_error
error_name (error.name, e.g. one of the ~20 typed producer error
classes) and failed_stage_code (job.currentStage/failedStage
normalized to a stable snake_case code via normalizeStageCode)
join the existing free-text error_message/failed_stage, so a fleet
query can bucket failures without parsing prose. Error-conditional by
nature — render_complete has no equivalent, since a successful render
has no error to name or stage to blame.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42
- Studio preview works for projects whose path has non-Latin-1 characters (#3982)
- Studio render downloads use RFC 6266 headers for non-Latin-1 filenames (#3984)
- check --json reports HDR auto-promotion; render prints one warning (#3985)
- Forced --sdr renders tone-map HDR sources before extraction (#3981)
- Render/preview workers no longer survive as orphans after Ctrl-C or parent exit;
cancellation reaches final artifact promotion; HYPERFRAMES_RENDER_DETACHED=1 for
intentionally detached renders (#3974)
- HDR preflight test coverage for held tails (#3963)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom and scope
- #3982 entry: a Studio preview server crash for compositions whose file path
has non-Latin-1 characters (the project directory is not involved)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom
- #3982 entry: the uncaught ETag TypeError took the preview server down, so
call it a Studio preview server crash rather than a preview failure
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Report HDR auto-promotion consistently from the render media set, including remote media, and make its regression coverage portable across supported paths.
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
Chromium's CDP layer can refuse a screenshot capture outright ("Protocol
error (Page.captureScreenshot): Unable to capture screenshot") with no
timeout wording at all. classifyCaptureFailure() didn't recognize the
message, so it fell through to the fatal "authoring" bucket and neither
capture-retry path (the disk-capture retry loop, or the streaming-capture
pinned-fallback retry) ever attempted a retry — including under
--low-memory-mode, whose single-worker plain-screenshot capture never
engages either path's other retry conditions.
Adds the literal error text to classifyCaptureFailure()'s transient_browser
bucket, and threads the resulting classification into
shouldRetryViaPinnedFallback() as a routing-independent retry condition, so
the streaming path recovers on a fresh session the same way the disk path
already does for other transient browser failures.
Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
* fix(render): keep verification fallback storage-aware
* fix(render): honour storage-aware verification fallback for every routing kind
The disk-headroom flag was computed by the orchestrator for both
worker_inversion and parallel_router routings but only honoured by
replanAfterFailure under worker_inversion, so the default-on
parallel_router path still reverted to an sdr_disk plan that the 90%
gate would reject and assertDiskCaptureHeadroom aborted the render.
Give the predicate a single owner: drawElementVerificationFailure
decides when headroom can change the fallback (non-default routing,
sdr_disk preferred fallback, off-disk memory-exhaustion fallback) and
consults the shared inspector only then. replanAfterFailure now honours
the resulting flag for any routing kind, and the orchestrator merges the
two revert log branches keyed on that flag instead of on plan kind,
which also stops OOM retries from being logged as headroom diversions.
inspectDiskCaptureHeadroom returns a discriminated union so the assert
no longer needs a redundant null guard to narrow freeBytes.
Tests: parallel_router headroom diversion and restoration, the failure
constructor across default / streaming-only / disk-only routings, and an
exact-once statfs count.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* test(render): prove non-verification capture failures skip the disk inspector
Extract the streaming drain's failure construction into
`streamingCaptureFailure` so the retry classification is unit-testable,
and assert that a drawElement capture failure or OOM on a routed plan
never calls the disk headroom inspector — only a self-verification
failure may, via `drawElementVerificationFailure`.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
A root-level `[data-start]` clip with no authored `position` starts out
`position: static` until the runtime forces it to `position: absolute`. A
visibility pass over the clip while it's still inactive can observe the
pre-forcing `static` value and cache it as in-flow, which correctly hides
the clip with `display: none`. But by the time the clip later becomes
active, that same cache has been invalidated and recomputed against the
now-forced `absolute` position — so the un-hide check, which re-derived the
same cached fact instead of tracking what it had actually done, saw
"not in-flow" and skipped the removal. The clip stayed `display: none` (and
therefore zero-sized) for the rest of the render.
Tracks whether this code applied `display: none` to a given element in its
own WeakSet instead of re-deriving the in-flow cache, so the un-hide check
can no longer disagree with the hide check that set it.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(producer): read useDrawElement post-clamp for the parallel-stream router
The non-DE parallel-streaming router read cfg.useDrawElement before the
default-on drawElement clamp ran, so on every macOS/GPU-default host with
multi-worker renders it saw a stale true and refused to stream even though
the render was about to fall back to non-DE screenshot capture anyway.
* refactor: trim fix-narrating comments to durable invariants
Adversarial simplify pass flagged two comments that described the diff
itself (what used to happen, what changed) instead of the invariant that
matters going forward. Kept only the forward-looking reasoning.
* fix(producer): capture probe console before closing on the DE clamp path; add regression coverage
- Extract the default-on drawElement clamp condition into a named, exported,
unit-tested pure predicate (shouldClampDefaultDrawElement), matching this
file's existing convention for the sibling router predicate.
- Preserve lastBrowserConsole when the clamp closes a drawElement probe
session, matching every other probe-close site in this function (an
adversarial review caught this: on the auto-worker-calibration path, a
render that throws after the clamp would report the calibration browser's
console instead of the probe's).
- Add tests proving the router requires the clamp's post-clamp output, not
its input — the exact ordering bug PRINFRA-689 was about.
* test: drop internal link and fix-narrating comment from new regression test
A test comment I added carried an internal Slack archive link (this is a
public repo) and described the diff instead of the invariant — the user
caught it in review after my own simplify pass had already run and missed
it, since that pass ran before this test file existed.
* refactor: collapse duplicated ordering-invariant comment into one place
Two comment blocks explained the same 'why deParallelStreamForced alone is
safe here' reasoning, one forward-referencing the other. Stated once.
* fix(build): stop generated files from being read half-written during the build
Two unrelated pull requests kept failing CI for reasons that had nothing to do
with their changes.
Typecheck failed with "TS1002: Unterminated string literal" pointing at a
generated file. The root build compiles several packages at the same time, and
more than one of them regenerates files under a package's src/generated while a
sibling's tsc is already reading them. A plain write empties the file and then
refills it, so for a few milliseconds what is on disk is the first half of the
new file. Whichever build read it in that gap saw a truncated file and stopped.
It never happened locally because locally nothing else is reading.
Every generator now writes the new version under a temporary name and then
renames it over the target, which the filesystem does in one step. A reader
either gets the whole previous version or the whole new one; there is no moment
where it can see half of either. Content that has not changed is not rewritten
at all, so repeat builds no longer touch these files. Four generators were
affected and all four now go through one shared helper, which is where the rule
lives from now on.
The build also rebuilt the core package a second time, in parallel with the
packages that read it. That second rebuild was already redundant, and removing
it takes the writers out of the window entirely. Renaming is what makes the file
safe; dropping the duplicate build makes the build shorter as well.
Separately, a linter performance test failed on four of the last ten failed runs
on main. It timed a scan with a stopwatch and demanded the result come in under
two seconds; one run took 3.7s. That is a statement about how busy the shared
runner was, not about the code, because a stopwatch also counts the time the
machine spent running someone else's job. The test now counts only the processor
time this process actually used, and checks that the cost grows in step with the
input rather than against a fixed number of milliseconds. Measured on a machine
under load, the stopwatch ratio for the same input reached 45x while the
processor-time ratio stayed at 20x.
Both fixes come with a check that fails if the fix is removed.
* fix(build): keep the shared write helper inside the core package
The helper the generators call had been placed in the repo-root scripts folder.
Every container image that builds a package copies the packages folders whole
but cherry-picks root scripts one file at a time, so the image builds failed on
a missing module. The repo already shows both halves of that convention: the one
root script a package build imports has a matching copy line in the image, and
cross-package imports into the core package need none.
The helper now lives with the core package's other build scripts, and the one
generator outside that package reaches it the way the engine package already
reaches core.
* test(engine): keep the probe cache bound test off the filesystem
The eviction test wrote, stat'ed and deleted 129 temp files to exercise an
in-memory LRU rule. Its runtime tracked filesystem contention rather than
the code under test, and on a busy Windows runner the file churn alone
pushed a ~300ms test past the 5s timeout, failing unrelated pull requests.
Cache identity comes from stat, so the test now synthesises stat results
and never touches disk. While here, the test also asserts the LRU touch:
re-probing an entry before the bound is hit must keep it resident and
evict the next-oldest one instead. The previous shape never hit the cache
during the fill, so that branch was untested.
* test(lint): keep the scaling check inside the scanner's linear range
The CPU-ratio version sampled 320k characters, where the output string's
own growth dominates and the whole test cost seconds of CPU; on a shared
runner that tripped the default test timeout, the failure this change
exists to remove. Sample 10k and 80k characters instead, where 8x input
measures ~8x and a quadratic scan still measures 60x or more, and give
the test an explicit timeout so a slow runner reports the ratio.
* test(core): snapshot file identity through a descriptor
The before/after inode and mtime checks read the file through a path stat
and then exercised the writer on the same path, which reads as a
check-then-use race to static analysis. Snapshot through an open
descriptor instead; the assertions are unchanged.
* fix(security): verify CDN script integrity before inlining
* fix(producer): normalize SRI algorithm case
* fix(producer): abort compilation on integrity mismatch
* fix(compiler): preserve nested script integrity requirements
* fix(compiler): defer local inlining until integrity is known
* test(producer): pin unpinned drawElement stall retry
* fix(engine): bound drawElement frames so a wedged renderer falls back instead of failing (#3172)
* fix(engine): bound drawElement frames so a wedged renderer falls back instead of failing
A single drawElement frame could kill an entire render. On one comp
(caption-editorial-emphasis) drawElementImage returns normally and the renderer
then stops draining its task queue: the setTimeout(…, 0) that schedules
toDataURL never fires, the capture page.evaluate never settles, and 60s later
the stage watchdog fails the whole render. Deterministic on 152.0.7977.30,
always the same frame. Root-cause detail in PRINFRA-488.
Two gaps, both closed here.
1. Nothing below the 60s stage watchdog bounded a frame. Adds a per-frame
deadline (HF_DE_FRAME_TIMEOUT_MS, default 15000, 0 disables) around the WHOLE
frame operation, not just the drawElementImage call — the stall surfaces at
whichever page round-trip comes next, and was observed at both the capture
and the seek's background-image decode. Bounding one call missed it.
Deliberately NO per-frame screenshot fallback for this error: once the
renderer stops scheduling it is wedged for every subsequent round-trip on
that page. Measured — the screenshot fallback blew the same deadline. The
frame fails fast instead, so the recovery that actually works can run.
2. That recovery was ineligible. shouldRetryViaPinnedFallback only retried a
generic capture failure when the worker count was PINNED by the inversion or
the router; a comp that engaged drawElement on the ordinary single-worker
path had no whole-render fallback at all. A renderer stall is now retryable
on any routing — the failure is a property of drawElement itself, and the
retry re-renders on a fresh page via screenshot. Reported as
de_fallback_reason "de_renderer_stall" so it is distinguishable from
capture_error in telemetry, and counted per-session as deFrameTimeouts.
Verified end to end on the repro: previously exit 1 with "stalled: no frame
progress for 60000ms"; now the deadline fires, the render retries via
screenshot, and RENDER_OK with a valid 1920x1080 / 240-frame / 8.0s MP4.
Tests cover the predicate both ways plus the cross-package error match. Engine
1,481 and producer 587 green (audioPadTrim.integration flakes only under
full-suite parallel load — passes in isolation, and on clean HEAD it is green
too, so it is not from this change).
Refs PRINFRA-488
* fix(engine,producer,cli): count drawElement frame timeouts where they happen
`session.deFrameTimeouts` was incremented inside `captureFrameCore`'s catch,
but the deadline races that function from OUTSIDE it — `withFrameDeadline` is
applied by `captureFrameToBuffer`, and `DeFrameTimeoutError` is constructed
nowhere else. So the branch was unreachable: on a wedged renderer the counter
stayed 0, the engine-side stall log never printed, and
`CapturePerfSummary.deFrameTimeouts` reported 0 on every affected render. The
end-to-end recovery worked throughout — `isDeRendererStallError` matches by
name at the producer — only the observability was dead.
Moved the increment and the log into an `onTimeout` hook that fires from the
deadline itself, deleted the unreachable branch and the now-unused
`isDeFrameTimeoutError`, and threaded the count out to
`render_completed.de_frame_timeouts` so the rate is graphable apart from the
other `capture_error` fallbacks. A stall always costs a whole-render re-run,
which is why it deserves its own series.
Also corrected the `fallbackReason` docstring, which still listed only
`psnr | blank | oom | capture_error` after this branch added
`de_renderer_stall`.
`withFrameDeadline` is exported for a two-case fake-timer test: the deadline
fires and calls `onTimeout` once, and a resolving inner promise passes its
value through with no late timer. The first case fails if the counter moves
back inside the work promise.
(cherry picked from commit ec2b2b5b2a)
* fix(render): retry sequential screenshot stalls
* fix(render): bound parallel worker capture phases
---------
Co-authored-by: Vance Ingalls <vance@heygen.com>