Root cause: the per-worker capture calls in captureFrameRange
(parallelCoordinator.ts) take no abort signal of their own, and only
checked `signal.aborted` BEFORE starting each frame — a no-op once a
worker is already awaiting an in-flight call. On WSL2, the native
drawElement/BeginFrame capture call can hang indefinitely at frame 0
with no error. The DE parallel-router's existing stall watchdog
(captureStreamingStage.ts) correctly fires `stallController.abort()`
after HF_DE_STALL_MS, but that abort had no way to reach a
worker already wedged inside a hung capture call — so
executeParallelCapture's Promise.all waited forever, the render hung
indefinitely, and the CLI's circuit breaker (which only runs after
executeRenderJob settles) never got a chance to trip.
Fix: race each per-frame capture call against the signal actually
firing (raceAgainstAbort), the same "can't cancel, only race" pattern
already used by the sequential capture path. Once the watchdog's abort
is observed, the wedged worker rejects, executeParallelCapture settles,
and the existing pinned-fallback retry / "reverted" outcome / circuit
breaker machinery (already correct) runs end to end.
Also widen the CLI breaker's trip condition from the literal string
"reverted" to "not a clean routed success", so any future non-success
outcome the observability layer records also latches the breaker
instead of silently falling through.
Closes#3441
Co-authored-by: Miga <noreply@anthropic.com>
Classify each <audio> element before Web Audio capture: same-origin,
CORS-opted-in, or a non-http(s) scheme stays on the primary
createMediaElementSource() path; cross-origin media without a
crossorigin opt-in withholds that call (the Web Audio spec makes such a
node output silence without throwing) and falls back to fetch +
decodeAudioData, preserving the FX graph whenever the server allows
CORS. Recheck the route at the transport's irreversible capture
boundary, and account for currentSrc, src, and <source> candidates the
same way the HTML resource-selection algorithm does.
Emit a stable preview diagnostic (`runtime_web_audio_bypass`) at media
discovery time, not only from playback scheduling, so `hyperframes
check` — which seeks but never plays — can surface it as a
`web_audio_bypass` finding. Diagnostics are suppressed during export
rendering, where the producer mixes audio offline and already applies
the FX chain. The existing non-unit-rate fail-closed rule stays scoped
to fx-chain/automation so this fix does not newly mute grouped or
above-unity tracks.
Takes over #3459 with the data-native-audio escape hatch removed per
review feedback: the automatic cross-origin detection already covers
the cases that mattered, so the extra per-element opt-in attribute,
its route-classifier branch, and its diagnostic path are dropped in
favor of a single automatic behavior.
Fixes#3458
Original-Author: desenmeng
Co-Authored-By: desenmeng <desenmeng@users.noreply.github.com>
Co-Authored-By: Miga <noreply@anthropic.com>
`docs/changelog.mdx` reached 512 KB and started failing the 500 KB pre-commit
check, so `chore: release v0.8.14` could only be committed by passing
`HF_MAX_NONLFS_KB`. Every release from here would need the same override, and
the file grows a few KB each time.
The check is for binaries. Its own error message says "large binaries are being
committed to git instead of LFS", and its header explains why: an ONNX model,
HDR-regression MP4s, demo clips, each of which lives in history forever and is
paid for by every clone. That cost is specific to binaries. Git delta-compresses
text, so release notes that grow a few KB per commit add a few KB to the pack,
while a binary of the same size re-enters the pack whole on every edit.
Text is now exempt regardless of size, detected with `grep -I` (a file with NUL
bytes is binary), the same heuristic git uses for "Binary files differ". The
binary rule, the `registry/` exemption, the LFS check and the size threshold are
all unchanged.
The alternative was an allowlist entry for the one file, which would leave the
next legitimately growing text file to hit the same wall and get the same
one-off exemption.
`scripts/check-large-files.sh` had no test. It has one now, wired into
`test:scripts` so it actually runs: an over-limit binary fails, an over-limit
text file passes, an under-limit binary passes, and multiple offenders are all
named. Verified the text case fails with the exemption removed, so the test
pins the behaviour rather than describing it.
`registry_item_added` fires when a catalog block is installed and
`render_complete` fires when a video is produced, but nothing joined them, so
"did this video use the catalog?" had no answer.
`hyperframes add` now records each installed item in `hyperframes.json`
(installed files are plain composition HTML with no provenance marker, so this
manifest is the only record that a file came from the registry), and
`render_complete` reports both the items the project installed and the blocks
the rendered composition actually reaches. An item installed and then never
mounted was tried and dropped, which no add-time event can express.
The scan answering "which sub-compositions does this file mount" now has one
owner, `collectSubCompositionSrcs` in `@hyperframes/parsers`, shared with
lint's `lintMissingOrEmptySubComposition`. It holds two invariants that were
previously restated per call site and got re-derived wrongly: it is a text scan
rather than a DOM query, because `<template>` content is inert and every
sub-composition except the render entry is wrapped in one; and references
resolve root-relative at every nesting level, matching `parseSubCompositions`.
It walks tag by tag rather than running open-ended spans across the whole file,
so a malformed composition cannot stall the render plan.
Also: `registryItems` is declared in the config schema, which closes with
`additionalProperties: false`, with an ajv-backed test pinning every key the CLI
writes; counts are never truncated by the name cap, and the reported used blocks
stay a subset of the reported installed ones, with `registry_items_truncated`
marking a windowed list; and an unreadable manifest reports itself rather than
posing as a project that never used the catalog.
The runtime absorbed a series of authoring mistakes over time and `runtime/init.ts`
says so in its own comments, but the skills kept teaching the old rules. Four of
them actively cost an agent a failing run: add `crossorigin` (lint rejects it
unconditionally), never build a timeline inside `async` (lint calls that the
documented contract), never `gsap.set` later-scene clips (two fixHints instruct
exactly that), and 12 copyable media snippets with no `id`, which render silent.
Corrected in every place each claim appeared, including `hyperframes-animation`,
three workflow scripts, the scaffolded project instructions, the CLI `docs`
command, and the public docs site: `data-track-index` is a Studio display lane
the render never reads, `class="clip"` is a layout convention rather than a
visibility requirement, timed elements may nest, the visibility window is
half-open, sub-composition host dimensions are backfilled, and the root-fill rule
applies only to the layered-composite path.
Behaviour changes, each backed by a render rather than by reading code:
- `timeline_registry_missing_init` deleted. The runtime creates the registry
before any inline script; a composition without the guard line renders and
animates correctly.
- `video_nested_in_timed_element` kept, message corrected. A rendered repro shows
the nested-with-local-start case really does break, so the rule guards a real
defect, but nothing is "FROZEN": the extractor ignores the wrapper's offset
while visibility uses it, so the clip shows wrong frames and then vanishes.
- `mediaRenderIds` now stamps media whose source is a `<source>` child, closing a
duplicate-id gap the old `[src]`-only selector left open.
- Stale messages fixed on `subcomposition_root_styled_by_class` and
`deprecated_data_layer`.
`coreSkillContent.test.ts` pinned the literal sentence that made root
`data-start` look required, so it is narrowed to structure plus the regression it
genuinely catches.
Not covered, and flagged in the PR: the media global-vs-local start heuristic in
`runtime/init.ts` is the root cause behind the nested-video defect. Removing it
changes the meaning of existing compositions and needs its own deprecation.
* feat(studio): drag automation segments
* fix(studio): clear clip selection for group effects
* fix(studio): replace clip selection with audio bus
* fix(studio): make audio bus selection authoritative
Grouped audio shipped and its three canaries were deleted rather than raised.
Solo and the group meter shipped and were removed in the same week, so the
digest says so. Notes that the v0.8.0 minor bump marks two catalog component
removals rather than the week's headline.
Generated with bun run changelog:weekly, then rewritten for publication.
Every sha was machine-verified: 40 chars, prefix-matched, an ancestor of main,
and inside the 2026-08-17..2026-08-25Z window. npx mint validate passes from docs/.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
seam-gate verify --project has failed with "preview server exited early" since #3310, which made `hyperframes preview` pick its launch mode from the TTY. The gate spawns with stdio [ignore,pipe,pipe], so interactive is false and it silently took the background path: the launcher exits 0 before the server is serving (read as a dead server), and the detached server escapes the gate's process-group cleanup (leaking a preview per run).
Pass the CLI's own --foreground opt-out in the default --server-cmd. Both skill mirrors updated.
— Rames
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* feat(lint): validate audio group membership and timing
* test(lint): pin audio group membership guards
* fix(studio): unify audio IDs and group state
* fix(studio): make audio-group edits transactional
* fix(studio): keep preview state synchronized
* fix(studio): align audio rows, automation lanes and headers
* fix(studio): stabilize timeline audio derivations
* refactor(studio): simplify group metadata memoization
* style(studio): keep timeline layout within size gate
* fix(studio): keep timeline preset apply off auditions
* fix(studio): harden carve and FX rack behavior
* fix(studio): repeat audio FX reveal requests
* fix(studio): reconnect property-panel audio controls
* fix(studio): unify property panel audio detection
* fix(studio): satisfy panel and deletion gates
* feat(studio,core)!: remove solo and the group meter
* docs(audio): keep removal rationale current
* refactor(core): retire studio solo bridge
* docs(audio): document grouped audio and its guardrails
* docs(audio): point handoff at replacement stack
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* feat(lint): validate audio group membership and timing
* test(lint): pin audio group membership guards
* fix(studio): unify audio IDs and group state
* fix(studio): make audio-group edits transactional
* fix(studio): keep preview state synchronized
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* feat(lint): validate audio group membership and timing
* test(lint): pin audio group membership guards
* fix(studio): unify audio IDs and group state
* fix(studio): make audio-group edits transactional
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* feat(lint): validate audio group membership and timing
* test(lint): pin audio group membership guards
* fix(studio): unify audio IDs and group state
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* feat(lint): validate audio group membership and timing
* test(lint): pin audio group membership guards
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
* fix(engine): harden grouped audio rendering
* docs(engine): explain grouped mix fallback invariant
* test(engine): allow grouped mixes to finish on Windows
* fix(core): harden audio FX and group identity
* fix(core): address audio group review feedback
* fix(core): align preview transport with grouped audio
* test(core): pin audio group gain ceiling
* fix(core): preserve solo bridge through stack
Reported as "the grouping button did nothing — I clicked it and nothing
happened". The dialog WAS opening. It positioned itself at
`anchorRect.bottom + 4` with no flip and no clamp, and this button lives in a
track header at the bottom of the studio window, so it opened past the viewport
edge. It was the last floating surface in the timeline with no viewport handling
at all.
It now goes through `resolveFloatingPanelPosition`, the helper the other body
portals already position with (`RenderQueue`, `propertyPanelColor`), so it flips
above the anchor when there is no room below and clamps so neither edge leaves
the viewport. `GROUP_DIALOG_SIZE` is a declared estimate in the same style as
`FORMAT_PANEL_SIZE` and `COLOR_PICKER_SIZE`: `w-56` is exact, only the flip
decision reads the height, and the clamp keeps the dialog on screen either way.
Two tests, at a realistic bottom-of-window anchor and hard against the right
edge. Both verified to fail against the raw positioning.
Worth noting why this shipped: the existing `group-pointer` test passes with or
without the fix. happy-dom reports an all-zero rect for an unlaid-out button, so
the dialog landed at top:4 — on screen, and nothing like the real app. A geometry
test that never sets a geometry proves nothing.
Deliberately NOT included: a toast for the grouping write's silent
`elements.length < 2` bail. That path is real in code but I could not reach it
from the UI — the button only renders on a track with 2+ ungrouped clips, and
sub-composition audio arrives as separate single-clip rows, so the offer never
appears there. Adding a message for an unreachable branch, plus the file split it
would force to stay under the 600-line studio cap, is not justified by evidence.
* test(sdk): render-faithfulness test for serialize() bake contract (WS-F)
Adds session.render-faithful.test.ts with 8 assertions covering the full
op batch (setStyle + setText + setTiming + addGsapTween + moveElement).
Confirms serialize() emits fully override-baked, render-ready HTML — this
is the SDK-side guarantee that the backend render input needs no separate
override-set field. Also asserts GSAP <script> edits and data-composition-
variables survive serialization unchanged.
Decision recorded: no SDK bake helper. session.serialize() IS the bake.
Content-address → zip → S3 upload → pointer-swap is host/backend (WS-P/WS-R).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(sdk): close the two false-positive slots in the bake-contract assertions
`toContain('data-end="5"')` in the setTiming case and `toContain('data-y="50"')`
in the full-batch case both match a DIFFERENT element in the fixture —
hf-title already ends at 5, hf-box already sits at y=50 — so either assertion
would still pass if its write regressed to a no-op. Paired each with the
disappearance of the target element's own pre-mutation value, which is the
pattern the setText case already uses.
* test(sdk): assert the canonical data-duration timing shape, not legacy data-end
The false-positive slot was hiding a real behaviour change. `setTiming` now
routes through the parsers' `writeClipTiming`, which canonicalizes timing onto
`data-start` + `data-duration` and REMOVES the legacy `data-end`. The
full-batch case asserted `data-end="4.5"` and failed once rebased onto main;
the standalone case asserted `data-end="5"` and passed only because hf-title
carries that exact value in the fixture — the very collision this pass set out
to close.
Both now assert the start/duration pair and the disappearance of the target's
own legacy end.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The render POST's catch took no binding, so the exception was discarded and
every transport failure produced one sentence: "Could not reach render server.
Use `hyperframes render` from the CLI instead."
A dead server, a DNS failure, an aborted request and a server that died
mid-render are all indistinguishable under that string — and it is not only a
UI message, it is what travels into the feedback report. Three separate field
reports carried it verbatim, one of them describing a render that fails every
single time. A guaranteed reproduction that tells us nothing is worse than an
intermittent one that does.
Bind the error and append it. The CLI guidance stays, since it is still the
right next step for the user; it just no longer stands alone.
Regression test asserts both halves: the cause appears, and the guidance
survives. It fails on the unfixed code with `expected 'Could not reach render
server. Use `h…' to contain 'Failed to fetch'`.
* docs(audio): document the audio effects system
The effects feature had no documentation at all — the only mentions anywhere in
docs/ were changelog entries. This adds the three pages the color-grading feature
already has, one per audience, and closes a gap in the existing audio guide.
- prompting/audio-effects — a new Level 5 chapter on asking for a mix in
symptoms rather than in filters, with the voiceover carve as the headline, the
level-before-depth check, groups, and the three requests that have no honest
answer (de-essing, noise removal, tone matching).
- studio/audio-effects — the rack, presets by symptom, the carve module, groups,
the deliberately asymmetric mute/solo, automation lanes, and troubleshooting.
- reference/audio-effects — the contract: all four attributes, every effect and
parameter range, why some parameters cannot be automated, the 19 presets, the
five jobs and five one-knob profiles, carve semantics, the group model, the
render bus, preview/render parity, and the three lint rules.
Also points the existing "duck music under important speech" advice in
guides/voice-and-audio at the carve, which does that properly and was never
named there.
Facts verified against source rather than the shipped skill prose: the panel
section is "Audio FX" (the neighbouring "Effects" section is visual effects, and
an earlier draft of this page named the wrong one), MAX_AUTOMATION_POINTS is 512,
fromPreset carries a preset id rather than a boolean, the leveller targets the
track's own 80th percentile, group mute reaches the render while solo never
leaves Studio, and the CLI carve currently writes clip ids.
No screenshots in this pass, and no placeholders for them either — the pages
carry concrete markup and tables instead. Screenshots of the rack, the carve
module, and a group row would each earn their place later.
mint validate and mint broken-links both pass.
* docs(audio): give audio its own Studio group, split by task
Review feedback: the audio page did not belong in Studio / Edit. That group is
already one task per page — canvas, timeline, animation, captions — and a single
page covering the rack, the carve, groups, and automation lanes was four tasks
bundled together and dropped in beside them.
Studio now has an Audio group holding four task pages:
- Effects and presets — the rack, presets by symptom, adding single effects in a
working order, the one-knob controls, Even Out Levels
- Voiceover carve — its own page, because it is the feature people come for
- Groups, mute, and solo — including why mute reaches the export and solo cannot
- Automation lanes — drawing envelopes, the shape menu, and which parameters
cannot move at all
Not a new top-level tab: the tabs here are audience-scoped (Guides, Studio,
Catalog, Developers), so a feature tab would be the only one of its kind and
would strand the prompting chapter out of its Level 5 sequence and the reference
page out of Developers.
Repointed the deep link in guides/voice-and-audio at the carve's own page.
Lane interactions verified in source before documenting: the right-click menu
offers Ramp up, Ramp down, Swell, Dip, and Simplify (which needs three points).
Deliberately not documented: "clicking a lane label reveals it in the rack",
which is not on main.
mint validate and mint broken-links both pass.
* docs(audio): fix the four review blockers
All four verified in source before fixing; the review was right on every count.
**The registry was incomplete.** `pitchshift` ships at `audioFx.ts:509-535`
(`semitones` −12–12, `mix` 0–1, worklet-backed so neither automatable) and was
missing entirely — sixteen effects, not fifteen. It also joins the worklet list,
so five effects expose no automatable parameters rather than four.
**Three presets were missing.** `chipmunk`, `giant`, and `monster` ship at
`audioFxPresets.ts:336-357`, all built on `pitchshift`. Twenty-two presets, and
Character holds ten. Fixed in both the reference table and the Studio list.
**The copyable markup contradicted the warning above it.** The page said
`carve.mjs` only finds double-quoted attributes and then gave three
single-quoted examples — copying the chain example would make a later carve miss
the existing chain and overwrite it. All three are now double-quoted with
`"`, each followed by its unescaped reading so it stays legible.
**The attribute table over-claimed.** `data-audio-group` is a plain id, not JSON,
and is ignored on `<video>`; the other three also live on `<hf-audio-group>` for
a group. The table now carries shape and valid host per attribute.
**The automation contract was wrong on two axes.** A clip lane's `t` is
clip-relative but a GROUP lane's is composition time, because a group has no
`data-start` (`webAudioTransport.ts:337-342`, `audioMixer.ts:1311-1344`) — both
pages now split the two clocks. And `volume` is not 0–1: the ceiling is
`MAX_AUDIO_GAIN`, +12 dB or about 3.981 (`audioGain.ts:8-9`), so a boosting lane
is valid and documented.
**Current-main drift.** #3416 is merged, so the CLI now records the voices'
shared group when it is safe and falls back to clip ids when that group contains
the bed or a music/SFX member. Documented, including why neither refusal shows
up on the run that writes it, and rebased onto main.
mint validate and mint broken-links both pass.
* docs(audio): name the real add-menu family, and finish propagating pitchshift
Second review round. All three findings were my own incomplete propagation — I
corrected the reference for `pitchshift` last round and left the reader-facing
pages behind it.
**The add-menu family is `Time`, not `Space`.** `propertyPanelFxAddMenu.tsx:22-28`
labels the four groups Filters / Dynamics / Non-linear / Time, and the time group
holds pitchshift, delay, chorus, phaser, and reverb. The Studio page sent readers
looking for a group that does not exist. It is now a table naming the family and
its contents, and the reference's "Time — space and width" heading is retitled,
since that description stopped covering the family the moment pitch shift joined
it.
Also from the same file: the menu offers the named jobs in place of a bare
`peaking`, because picking `peaking` is picking a machine and leaving the real
decision — which range — for afterwards. Worth saying on the task page.
**Pitch shift was missing from both no-automation lists** that a reader actually
follows — `studio/audio-automation` and, unflagged but the same defect, the
prompting chapter. Five worklet effects in all four places now. Called out
explicitly on the Studio page, because a rising pitch is exactly the thing
someone reaches for a lane to do, and the lane will not report that it cannot.
**Narrowed the group-metadata sentence.** "The other three are JSON, and on a
group they live on `<hf-audio-group>`" swept in `data-fx-carve`, contradicting the
table directly above it. Only `data-fx-chain` and `data-automation` are group
metadata.
mint validate and mint broken-links both pass.