5059 Commits
Author SHA1 Message Date
Miguel Ángel eba596d026 chore: release v0.8.212 (#6660) 2026-10-28 12:29:35 +00:00
James RussoandClaude Opus 5.5 2d3033c719 fix(cli): exit 2 for remaining flag validation errors (#6639)
* fix(cli): exit 2 for remaining flag validation errors

Flag and argument checks that commands make themselves still exited 1 and
reported runtime_error under --json. They now take the existing usage-error
path (failUsage / CliUsageError): exit 2 and a usage_error envelope. This
covers lambda and cloudrun render/render-batch (--width/--height, --fps,
positive-number, enum and --output-resolution flags, unknown subcommands),
capture, feedback, benchmark --runs, tts, init, snapshot, compare,
grade-compare, check, normalize-audio, media-treatment, play/present --port,
models, docs, browser and telemetry unknown subcommands, cloud render
conflicting inputs, cloud delete without --no-confirm, and inline --variables.
A few checks now run before project or stack lookup so a bad flag is reported
first. Runtime failures (missing files, network, render errors) still exit 1.

An unattended spinner now writes its start and progress text as plain stderr
lines, not only the outcome, so tools watching init can see "Transcoding".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): reject a bad snapshot --frames, quiet spinner progress, drop added comments

- `snapshot --frames abc` (or 0, -3, 2.5) is now a usage error: exit 2, and a
  usage_error envelope under --json. It used to fall back to 5 frames silently.
- An unattended spinner prints its start line once and its outcome, but no
  longer one line per progress update, which flooded stderr during
  remove-background and the Chrome download.
- Removes the comment lines this branch added, so the comment ratchet passes.
- The render --json test now expects exit 2 and usage_error for bad inline
  --variables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 11:27:49 +00:00
Miguel Ángel b84762230f fix(cli): snapshot warns when a video on screen has no source to read a frame from (#6656) 2026-10-28 11:14:19 +00:00
James RussoandClaude Opus 5.5 85efaece7e fix(cli): every --json failure answers with one envelope, and --json only where supported (#6622)
* fix(cli): every --json failure answers with one envelope, and --json only where supported

Under --json, failures came back as a bare string (`"error": "Command failed"`
from lint), a command-specific shape, or nothing at all on stdout (info,
compositions, render, snapshot, cloud get). Now every failure is
`{"ok": false, "error": {"code", "message", "hint"?}}`:

- jsonFailure() in utils/jsonError.ts builds it. The commands that printed
  `{ok: false, error: "<text>"}` use it, keeping their extra fields (lint's
  findings, validate's errors). A refused timeline edit now writes it to stdout
  instead of stderr, and keeps its `reason` and `fix` fields.
- When a --json run fails without writing anything to stdout, cli.ts writes the
  envelope itself: usage errors carry their own code (unknown_flag,
  unknown_command, json_unsupported, confirmation_required), anything else is
  runtime_error with the message the command printed on stderr.

--json is no longer accepted by every command. A command supports it by
declaring a `json` arg; the others reject it with exit 2 and the code
json_unsupported, instead of accepting the flag and printing prose.

The contract test now checks every command's --json path, and packages/cli
gets an AGENTS.md with the rules: stdout is data, stderr is messages, never
prompt without a TTY, exit 2 for usage errors, --json always returns an
envelope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): put the real failure in the --json envelope, never the generic text or a notice

lint --json and check --json in a directory with no project answered
"message": "Command failed". The project check reports the problem, then
throws a presented "Command failed" with the real error as its cause, and the
command's own catch serialized that generic text. failureMessage() now returns
the cause, or else what the command reported through errorBox, and every
command catch that writes a --json failure uses it (lint, check, validate,
layout, snapshot, compare, grade-compare, media-treatment, tts,
remove-background, add, skills, lambda policies). compare, grade-compare and
add share failCaught() for that catch.

The boundary envelope follows the same order: the error's own message unless
it is the generic one, then its cause, then the errorBox title and hint, and
only then the stderr text. Notices (the telemetry disclosure, deprecation
lines, diag progress) are no longer captured, so validate, layout and inspect
no longer put "is deprecated" in the error message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): render --json keeps human output off stdout so a failure answers with the envelope

Under --json a single render printed its header, lint findings and progress on
stdout, so a later failure such as `render --variables '{bad' --json` ended
with no JSON envelope. --json now silences that output the way --batch --json
already did, and --variables is parsed before the browser check so a bad value
fails without a Chrome download.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): a --strict lint abort says why, even under --json

Under --json the render is quiet, so a --strict lint abort printed nothing and
the envelope said only "Command failed". The abort is now an error box on
stderr in every mode, naming the error and warning counts and the first
finding, and the --json envelope carries the same message. Lint now runs
before the browser check, so the abort never waits on a Chrome download.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cli): time the render setup cancel from the wrapper's exit

The 1 s budget started when the command did. With lint now running before the
browser check, its subprocess (about 250 ms here) sat inside that budget, and
the test failed once under load. The budget now starts when the wrapper exits,
which is the latency the test is about.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 10:42:10 +00:00
Miguel Ángel b6eaed7cae fix(cli): a check page forces a WebGL drawing buffer but keeps no list of 2d canvases (#6650) 2026-10-28 10:41:22 +00:00
Xuanru Li 205c1a31d3 fix(inspector): live queries see every composition, wherever the playhead is (#6651)
* fix(inspector): live queries see every composition, wherever the playhead is

The Studio preview parks scenes away from the playhead, so a live inspect, timeline or
element-map query answered missing for every element in a parked composition. Each live
query now holds every scene mounted under its own holder, then hands the scene window back.
__hfHoldScenes keeps holds per holder, so the inspector letting go never drops a scene
Studio holds.

* fix(inspector): isolate concurrent live query scene holds
2026-10-28 10:39:49 +00:00
Miguel Ángel ecad35a225 fix(cli): resume a model download that stalls partway instead of failing (#6649)
* fix(cli): resume a model download that stalls partway instead of failing

* fix(cli): pin the idle-only timeout; one-line doc for the resume

* fix(cli): resume from the bytes on disk and check the size before caching

* refactor(cli): split the download response handling into named steps
2026-10-28 10:27:00 +00:00
Miguel Ángel 7112e4b6bf test(studio): wait for the first undo press before the edit that should block the second (#6652) 2026-10-28 10:19:07 +00:00
Vance IngallsandClaude Opus 5.5 a88751d9d6 fix(studio): a thumbnail reader forgets a start's failures after a minute (#6642)
* fix(studio): a thumbnail reader forgets a start's failures after a minute

A pooled thumbnail reader lives for as long as clips keep asking it, and each start
byte has a retry budget of two. That budget never reset, so two transient failures at
one start early in a session made a third, twenty minutes later after the cache evicted
it, give up at once: the reader retired and the clip showed an error.

A start's failures now count for a minute. One bout of retries (8 s stalls, 0.5 s and
1 s waits) fits inside it, so a start that keeps failing still gives up; one met again
much later starts afresh. Clearing the count when a read from that start delivers bytes
would not do: a server that ignores Range sends bytes 0 onward again before it breaks at
the same byte, and would retry forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): the start-failure window says what it is in its name, not a comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): a start's retry budget resets when a read from it ends cleanly, not after a minute

The one-minute window only held while each attempt took under a minute. An attempt has
no limit before its answer, and a body that keeps sending never stalls, so a server that
ignores Range, sends slowly and breaks past a minute re-downloaded from 0 forever.

A start's failures are now forgotten when a read from it ends cleanly: its body finishes,
or mediabunny cancels it at its target. A server that breaks before the target never ends
a read cleanly, so its budget still runs out, however slowly it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): a read mediabunny stops at its target ends cleanly and resets its start's budget

mediabunny's UrlSource never cancels the body: once a read reaches its target, _runWorker
aborts the signal it passed in, and the pending pull fails with an AbortError. So the reset
on cancel never fired, and any open bytes=N- read stopped at its target kept its start's
failures.

The request now listens on its own signal, which follows mediabunny's and is unlinked once
the read ends: an abort while live counts as a clean end only when mediabunny's signal is
the one aborted, never a stall or a dispose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:15:28 +00:00
Miguel Ángel 5a0179b88e fix(studio-server): the render route passes a MOV ProRes profile to the render (#6644)
* fix(studio-server): the render route passes a MOV ProRes profile to the render

* fix(studio-server): drop the added comment line so the comment share does not rise
2026-10-28 09:44:32 +00:00
Miguel Ángel 7758d9b8fd fix(engine): a Windows browser that will not start says where and why (#6647)
* fix(cli): a Windows browser that will not start says where and why, not just spawn UNKNOWN

* fix(engine): the launch diagnosis names a browser file Windows refuses to run

* fix(engine): the launch diagnosis names a browser file Windows refuses to run (engine part)
2026-10-28 09:41:17 +00:00
James RussoandClaude Opus 5.5 e2c9151320 fix(cli): add help [cmd], suggest the closest command or flag, and quiet the nags (#6621)
* fix(cli): add help [cmd], suggest the closest command or flag, and quiet the nags

`hyperframes help` printed "Unknown command help" with raw ANSI codes and no
suggestion. It is now an alias: `help [cmd...]` is `[cmd...] --help`.

An unknown command (at the root or inside a group like `cloud`) prints
"Unknown command: rendr", "Did you mean 'hyperframes render'?" and the group's
usage, all on stderr, and exits 2. An unknown flag suggests the flag it most
likely means ("Did you mean --output?"). Usage text printed for a usage error
now goes to stderr instead of stdout, and citty's colors are stripped from
help whenever this CLI's own colors are off.

No update, skills or app-history notice is appended to a usage error. The
skills notice shows at most once a day per project (timestamps in
~/.hyperframes/skills-notice.json) and, in an unattended run, only when a core
skill is missing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): reject an unknown top-level command without loading every command

citty looks for an alias on every registered command before it reports an
unknown name, which loads all of them. From a source checkout without the
studio packages built, that meant `hyperframes rendr` failed in preview's
build check with exit 1 instead of suggesting `render`. The name is now
checked against the registry first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): help with a mistyped subcommand reports it like the command itself

`hyperframes help cloud rendr` printed cloud's help and exited 0, while
`hyperframes cloud rendr` exits 2 with a suggestion. Both now exit 2 with the
suggestion. A group that also takes positional arguments (timeline takes a
project directory) still shows its help for a word it does not know.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 09:39:06 +00:00
Miguel Ángel 3d1c0764a0 fix(cli): check prints a progress line as each pass after overlap begins (#6646)
* fix(cli): check prints a progress line as each pass after overlap begins

* fix(cli): check counts finished passes and drops the comment the ratchet flagged
2026-10-28 09:37:17 +00:00
Miguel Ángel 5ed694264e chore: release v0.8.211 (#6623) 2026-10-28 09:30:08 +00:00
Miguel Ángel 6553b1f6ba perf(producer): --gpu renders encode while frames are captured (#6631) 2026-10-28 09:24:27 +00:00
Vance IngallsandClaude Opus 5.5 fa137cdfef fix(studio): a slow thumbnail job keeps a reader that other clips still decode from (#6643)
* fix(studio): a slow thumbnail job keeps a reader that other clips still decode from

A job that timed out judged the reader by its own progress only. A poster job that got
an already-open reader while a sibling was decoding on it, and took more than its
budget to reach its one frame (a slow link, Chrome's six connections held), logged a
timeout and retired a healthy reader, so the next clip re-read the index.

The pooled reader now counts the frames every job decodes from it, and a timed-out job
retires it only when no frame came from it while the job held it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): the reader's frame count says whose frames it counts in its name, not a comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:18:55 +00:00
Miguel Ángel 30df5a48af fix(studio-server): undo and redo on Windows restore a file something holds open (#6628)
* fix(studio-server): undo and redo on Windows restore a file something holds open

* fix(studio-server): a restore waits for the preview's reads to close before replacing the file

* fix(studio-server): a restore checks the project folder after the preview's reads close
2026-10-28 09:12:54 +00:00
Miguel Ángel 51f9fa7c54 perf(aws-lambda): small-chunk cloud renders stitch their output in parallel (#6635)
* perf(aws-lambda): small-chunk cloud renders stitch their output in parallel

When every chunk's payload is under S3's 5 MiB part minimum (typical for short 4K chunks), the assemble step could not copy any part server-side and pulled the whole output through one Lambda's network link. Those range-only parts now go to parallel invocations of the warm function that just rendered the chunks, about 32 MiB each and never more at once than the stack's reserved concurrency allows. The output bytes are unchanged.

* fix(aws-lambda): a throttled stitch helper builds its parts in the assemble instead

Another render can hold the stack's reserved concurrency while this one assembles, so a helper invocation may be refused. The assemble now builds that batch itself, logs it, and counts it in assemble_stages; the helper's error type is kept as the error name. Comments trimmed back to each file's share at the base.

* chore(aws-lambda): raise the package size budget to the measured size plus headroom

The parallel stitch helpers add about 1.7 KB packed and 9.9 KB unpacked; the new budget is the measured size times the checker's own 1.05 headroom.
2026-10-28 09:03:42 +00:00
Miguel Ángel 8e63faf846 perf(engine): a --gpu render probes the GPU encoders once, not twice (#6638)
* perf(engine,cli): a machine's GPU encoder probe runs once, not on every render

* refactor(engine): one shared probe per process, no answer kept on disk
2026-10-28 08:44:28 +00:00
Miguel Ángel e6476c2cd5 perf(engine): Macs with a hardware GPU render with more capture workers (#6636)
* perf(engine): Macs with a hardware GPU render with more capture workers

* perf(engine): Apple-GPU worker sizing skips layered renders; fewer comment lines

* test(producer): format
2026-10-28 08:42:31 +00:00
Miguel Ángel 2ddf41eb7d perf(producer): download a remote video once per render (#6637) 2026-10-28 08:04:05 +00:00
James RussoandClaude Opus 5.5 0bdc128cb5 fix(cli): never prompt or auto-install when nobody is at the terminal (#6617)
* fix(cli): never prompt or auto-install when nobody is at the terminal

publish and upgrade called the confirmation prompt with no terminal check.
With stdin from /dev/null, publish exited 13; with an open pipe it hung.
Unattended and without --yes, both now exit 2 before doing any work, and
stderr names --yes (upgrade also names --check).

All prompts and spinners now go through ui/prompts.ts, the only module allowed
to import @clack/prompts (a lint rule and a contract test enforce it). Its
confirm, select, text and password refuse to prompt in an unattended run, and
its spinner prints only the final line, on stderr. init, feedback, catalog and
cloud delete decide whether to ask with isUnattended() instead of a TTY check
of their own.

The background auto-install no longer starts in an unattended run, and the
update notice ("Run: npm install -g ...") is not shown while an install for
that version is pending. CI now counts as set for any value but "false" or "0".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cli): neutralize agent detection in the catalog offer tests through isAgentDriven

Attendance now reads isAgentDriven(), so that is what the catalog tests stub to
stay independent of the agent running the suite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 07:56:20 +00:00
Miguel Ángel 6cf4b56697 fix(engine): a render on Windows opens no console windows (#6613)
* ci(windows): probe that a render opens no visible window

* ci(windows): the probe names what opened each window

* ci(windows): the probe spawns each binary alone, with and without detached

* ci(windows): the probe logs the processes started during the render

* ci(windows): the probe also spawns Chrome one consoleless hop down

* ci(windows): the probe logs each child started without windowsHide

* ci(windows): the probe logs every child process with its windowsHide and detached

* fix(engine): Chrome keeps the CLI's console on Windows, so a render opens no window

* fix(cli): every child the CLI starts on Windows is hidden unless it opts out

* ci(windows): the probe bisects Chrome's flags and keeps each command's output

* ci(windows): the probe names the Chrome process behind each window

* ci(windows): the probe loads a page in Chrome directly and one hidden hop down

* ci(windows): the probe prints the Chrome main command line and renders with the reporter's flags

* ci(windows): the probe narrows the CLI's Chrome arguments to the ones that open a window

* ci(windows): the probe launches the CLI's Chrome arguments one hidden hop down

* ci(windows): the probe launches Chrome through the engine's browser pool

* ci(windows): the probe varies what the engine passes to Puppeteer

* fix(engine): renders on Windows launch Chrome with a Puppeteer that keeps it attached

* refactor(cli): hideChildConsoles copies its arguments and keeps one comment line
2026-10-28 07:48:40 +00:00
Miguel Ángel 1d97ed24c6 perf(producer): the audio mix runs while capture starts its browsers (#6632)
* perf(producer): the audio mix runs while capture starts its browsers

* test(producer): master-bus test stops the render at capture setup
2026-10-28 07:36:32 +00:00
Miguel Ángel 3cc296845a feat(core): name why a choppy preview window dropped frames (#6634)
* feat(core): name why a choppy preview window dropped frames

The playback reading now says how much of the window was frames skipped
and how much was a playing video waiting for data, and carries the p50
and p95 gap between the runtime's ticks, so a report can tell a slow
compositor from a stalled decoder.

* chore(core): drop comments the ratchet counts
2026-10-28 07:25:00 +00:00
Miguel Ángel 7d83a3d219 perf(producer): drawElement films keep their parallel workers instead of dropping to one (#6633)
* perf(producer): drawElement films keep their parallel workers instead of dropping to one

* perf(producer): shorter inversion floor comment

* test(producer): inversion suites opt into the single-worker inversion
2026-10-28 07:24:46 +00:00
Miguel Ángel 2b6d176452 perf(producer): worker sizing measures screenshot capture, one browser launch fewer (#6630) 2026-10-28 07:24:34 +00:00
Ular Kimsanov dd7290ae83 fix(studio): the desktop card says the download opens hyperframes.studio (#6594) 2026-10-28 06:58:39 +00:00
Miguel Ángel b288ce0187 fix(cli): snapshot prints a progress line as each frame starts (#6618)
* fix(cli): snapshot prints a progress line as each frame starts

* test(cli): contact sheet tests ignore snapshot's progress lines
2026-10-28 06:48:26 +00:00
Vance IngallsandClaude Opus 5.5 a54921e0f6 perf(studio): share video sources and keep frames for timeline thumbnails (#6629)
* perf(studio): keep released timeline video frames for scroll-back

A video thumbnail frame was revoked the moment no mounted strip held it, so
scrolling back past the scheduler's cache decoded every clip again: 80 decode
passes for 40 clips on a 44-minute source, the whole first pass repeated.

Released frames now stay shared in a least-recently-released list capped by
the new sharedFrameCacheBytes budget (32 MiB of JPEG, about 11 frames for
each of a 473-clip episode's clips). Object URLs are revoked only on
eviction, and a frame any strip still shows is never evicted. Which frame a
strip shows is unchanged: the acceptance window is the same predicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(studio): open each video source once for all its timeline clips

Every clip's thumbnail job opened its own mediabunny Input on the source URL,
so 40 clips cut from one 44-minute source opened it 80 times, re-reading and
re-parsing its 1.44 MB index each time (behind a 304, at 13-30 ms an open).

Jobs now share one pooled reader per source: the Input, its primary video
track and the track's size and duration, reference-counted and disposed
thumbnailSourceIdleMs (10 s) after the last job ends, or at once when the
source's contentVersion changes. Each job keeps its own CanvasSink: the sink
reuses one pooled canvas and a job blanks the canvases it saw on release, so
sharing one across jobs would corrupt a neighbour's frames.

A failed open leaves the pool at once, and a job that fails for any reason but
cancellation retires its reader, so the next clip opens afresh. Retries are
still counted per reader, as mediaUrlSourceOptions has done since #6477: two
jobs reading through one reader cannot tell whose read failed, and retiring a
reader whose retries are spent keeps that budget per reader rather than letting
a spent count fail later clips at once. The pool also owns the one
import("mediabunny"), which the decoder now takes from the opened source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(studio): start no timeline video decodes while the preview plays

A playing preview shares the machine's video decoder with timeline
thumbnails, and a long many-clip timeline queues hundreds of thumbnail decodes.
While the player store reports isPlaying, the scheduler now starts no new
visible or overscan video decode; a scrub's interaction request still starts,
decodes already running finish, and pausing drains the queue. Image,
composition and waveform work is unaffected.

The new thumbnailPlaybackHold module subscribes to the store's state rather
than wrapping setIsPlaying, so a reset or a project switch, which write
isPlaying: false directly, also release the held work. VideoThumbnail binds it
to the app's scheduler where video thumbnails load, which is also the path
HyperFrames Desktop runs (it embeds StudioApp, not Studio's main.tsx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(studio): bound thumbnail range reads and their read cache

mediabunny reads a URL with open-ended `Range: bytes=N-` requests and cancels
each once it has the bytes it wanted, by when a local server has streamed
megabytes past them: about 4 MB sent for each thumbnail frame decoded. The
thumbnail reader's fetch now asks for at most 512 KiB from N; a shorter answer
than mediabunny wanted makes it ask again from where it stopped (its worker
resumes a capped response when the file size is known, which the first
206's Content-Range supplies). Closed ranges and the probe reader are untouched,
and so is Studio's playback, which does not read through mediaUrlSourceOptions.

Measured on two 44-minute 1080p sources, 40 clips each (a poster and a
4-frame strip per clip), against the same branch unbounded: bytes sent fall
13-22% one decode at a time and 33% two at a time; first-pass wall time falls
4-20%. The frames decoded are byte-identical. Most of what remains is real:
a strip slot whose keyframe is outside its window decodes from that keyframe
forward through its GOP.

A pooled reader also now caches at most 16 MiB of what it read, not
mediabunny's default 64 MiB, since it outlives its clip by up to
thumbnailSourceIdleMs. 8 MiB cost requests when a clip's poster and strip
decode together; 16 MiB measured the same as 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): bound thumbnail ranges only once Content-Range is readable

A thumbnail reader bounded even its first range request to 512 KiB. When the
answer's Content-Range cannot be read, mediabunny sizes the file as the request
start plus the answer's Content-Length, assuming the answer runs to the end of
the file, so a bounded first answer made a 10 MB file end at 512 KiB. Browsers
hide Content-Range cross-origin unless the server lists it in
Access-Control-Expose-Headers, and Studio reads remote https sources such as
HeyGen's gen-os-static bucket, which does not. With the header hidden, every
format failed: faststart and fragmented MP4 decoded 1 of 12 thumbnails, and
moov-at-end MP4, MOV and WebM none ("no decodable video track").

A reader now bounds its ranges only after an answer of its own was a 206 whose
Content-Range it could read and that starts where the request asked; its
first request stays open-ended, as before the bound. Any answer without such
a Content-Range, a whole-file 200 included, keeps that reader unbounded for
good. Same-origin readers, where Content-Range is readable, still bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): count a thumbnail reader's read failures consecutively

A reader's retry budget (two retries, from #6477) counted every failure over
the reader's life. Pooled, a reader serves every clip of its source, so the
third transient failure anywhere on the source failed a clip with no retry.
The count now resets whenever a read delivers bytes, so a long-lived reader
keeps the per-clip tolerance a reader had when each clip opened its own, and
three failures in a row still give up. Bytes, not an ok status: a body that
breaks on its first read must still use up the budget. A reader that gives up
is retired, as before, so the next clip opens a fresh one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): a read stalled mid-response no longer hangs its video source

The thumbnail job's signal reached only the source's open: keyframe lookups and
the decode loop ignore it, so a job whose response stalled mid-body never ended
and never released its pooled reader. The reader was never idle, so never
closed, and every later clip that needed the same bytes waited on the same
stalled read for the rest of the session. Before pooling, each clip had its own
reader, so only the stalled clip hung.

The scheduler's timeout now aborts with a TimeoutError reason. A job that times
out drops its reader at once; a job cancelled any other way that has still not
ended a load timeout later drops it then; a cancel the job answers, a scroll
away, leaves the reader pooled. Dropping disposes the reader whoever still uses
it, which fails their pending reads too.

Disposing a mediabunny Input does not end a read already waiting on a stalled
body (its orchestrator only flags the worker, which never wakes), so
mediaUrlSourceOptions gains cancelReads(), which aborts every fetch the reader
has in flight; the worker's read then fails and mediabunny rejects the reads
waiting on it. Measured in Chromium with a server that stalls one response:
the stalled clip settles at its timeout, and the same clip asked again, now
and after 11 s idle, decodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): a clip's own failed result keeps its source's reader

A thumbnail job that failed for any reason but a cancel retired its pooled
reader, so one clip whose frames all failed to decode, or whose canvas would
not encode, made the next clip re-read and re-parse the source's index. Those
two failures are now a ClipResultError, which leaves the reader pooled; fetch,
demux and decoder failures still retire it.

Also pins four pool behaviours mutation testing found unguarded: a reader
closed after sitting idle has left the pool, so the next clip opens a live
one; a version change closes the old reader only once the job still decoding
on it ends; a job cancelled while the track is read stops the open there; and
a scroll-away cancel keeps the reader (the last landed with the stall fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep a thumbnail reader's retry budget per start byte

ff16aed98 reset a pooled reader's retry count whenever any read delivered
bytes. That broke the #6477 give-up in two ways, both measured with real
mediabunny:

- A server that ignores Range answers 200 from byte 0, so mediabunny resumes
  at the bytes it got and is sent byte 0 again. With a body that breaks after
  64 KiB, those bytes reset the count every time: 61 fetches in 30 s and the
  read still pending.
- mediabunny reads with two workers. A healthy stream in one region kept
  forgiving a region that always answered 503: 41 fetches in 20 s, pending.

Failures are now counted per start byte, with no reset. A resume after
progress asks for a new start and gets its own two retries; asking for the
same start again uses them up whatever else the reader reads meanwhile. The
start is the byte the answer began at: the asked start for a 206 or a failed
request, 0 for a 200. Traced for the 200 case: requests at 0, 65536 and 65536
are all keyed 0, so the third failure gives up. Both repros now give up after
3 fetches ("network TypeError after 2 retries", "HTTP 503 after 2 retries"),
and failures at different starts, one per clip, never add up.

The fetch and the answer's typing move into typedFetch and typedResponse, so
fetchFn stays under the complexity gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): fail a stalled thumbnail read on silence, not on a timeout

dbd820706 dropped a reader when the job using it timed out, but only once the
reader had opened: a timeout during the shared open released the job and left
the stalled open pooled, so every clip of that source joined it and timed out
in turn while clips kept arriving. Dropping on a timeout was also wrong when
bytes were still coming (a slow link's 25 s open threw its parsed index away),
and dropping a reader other clips were using failed them with "Media reads
cancelled", leaving them on "no preview" until they remounted.

A thumbnail read now fails only on silence. A request waiting 8 s for its
answer, or for its next bytes, is aborted and fails as a MediaFetchError
"stalled", which mediabunny retries like any read failure: a resume after
progress asks for a new start and continues, and three silent waits at one
start (8 + 0.5 + 8 + 1 + 8 = 25.5 s, inside the 30 s load timeout) give up,
which retires the reader. This covers the open and the decode alike, keeps a
reader whose bytes are still arriving, and never disposes a reader another
clip is using, so the job-level abandon and the scheduler's TimeoutError
reason are removed. Measured in Chromium against a stalling server:

- one-shot stall in the open: the clip decodes at 8.7 s (was: it and the next
  clip both timed out at 30 s);
- one-shot stall mid-decode: decodes at 8.7 s (main: hangs);
- a source that never sends a byte: each clip fails typed at 25.5 s and the
  next opens a fresh reader.

Each request gets its own AbortController, which follows mediabunny's signal
and the reader's cancel without AbortSignal.any; every way a request ends
unlinks it from the reader's long-lived cancel signal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(studio): pin the thumbnail behaviours mutation testing left unguarded

- A bounded answer's Content-Range keeps the reader bounded: a closed-range
  pattern that matched only open ranges flipped it unbounded after the first
  bounded request, undoing cf8f4554e.
- A frame that will not encode, through toBlob or OffscreenCanvas
  convertToBlob, is the clip's failure and keeps its reader. The
  convertToBlob rejection is now a ClipResultError too; it escaped as the
  browser's own error and retired the reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(studio): pin the stall watchdog's guarantees about its own requests

Four guarantees of the thumbnail reader's per-request watchdog had no test;
each mutant that broke one passed the suite:

- a silent body fails as "stalled" and aborts the fetch under it, so the
  stalled connection is not left open;
- every way a request ends unlinks it from mediabunny's signal: an answered
  body, a rejected fetch, a 503, no body, a broken body, a cancel;
- a probe reader's body is never timed, however slow;
- a request made after the reader's reads were cancelled starts aborted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): time a thumbnail read's silence only once its answer has started

The 8 s stall rule also timed the wait for a response's headers, and that
wait includes Chrome's queue for one of six HTTP/1.1 connections per host.
Desktop serves all media from one loopback host, where Studio's event stream
and streaming previews hold connections. Measured in Chromium against a
healthy server: with five of six connections held and 20 cancel/re-ask bursts
over the 40-clip pass, no false stall; with all six held for 40 s, two clips
failed as "stalled". The watchdog now times only a started body.

A server that never answers is now caught at the job's own timeout. The
scheduler's timeout again aborts with a TimeoutError reason. A job that timed
out without progress in its whole run (its source never finished opening, or
it decoded no frame) failed because of its source: a source that never
answers, or one that trickles bytes and never finishes. It logs one typed
MediaFetchError line ("network timeout") and retires its reader. A job that
waited for an open that finished, or decoded a frame, timed out on a slow link
and keeps the reader and its parsed index. Bytes alone are not progress, or a
trickling source would keep its reader for good.

An aborted job also ends its claim on the reader at the abort, rather than
when it settles: a job stuck in a read that ignores its signal no longer keeps
the reader open. A retired reader still waits for the clips decoding on it.

Measured in Chromium: a source that never answers fails each clip with one
typed line at 30 s and the next opens a fresh reader; with all six
connections held for 40 s, the two starved clips fail typed, none as a stall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): move the thumbnail reader's explanations into names

The comment ratchet (scripts/comment-ratchet.mjs) fails a source file whose
share of comment lines rises over its merge-base, and a new file above its
package's share. Five files of this branch did. The reasoning they carried is
in the commit messages and the PR description; what stays in code is named:
SILENT_BODY_STALL_MS, POOLED_READER_CACHE_BYTES, failuresByStart,
inFlightReads, decodedByJob, timeoutWithoutProgress and madeProgress. No
behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:37:40 +00:00
James RussoandClaude Opus 5.5 22e6da0233 feat(cli): define every command through one factory and test them all as an agent (#6616)
* feat(cli): define every command through one factory and test them all as an agent

Every command now comes from defineCommand in utils/defineCommand.ts. A lint
rule bans importing defineCommand from citty anywhere else, and a new contract
test walks the command registry in cli.ts, checks each command was built by the
factory, and runs it the way a coding agent does: no terminal, stdin closed,
CI=1, CLAUDECODE=1 and a home folder of its own.

Usage errors now exit 2: unknown flags, extra positionals, and citty's own
parse errors (missing argument, bad value, unknown or missing subcommand).
An unknown flag names the help command instead of printing nothing else.

isUnattended() in utils/attendedTerminal.ts is the one answer to "can a person
respond here". It is true without a TTY on stdin or stdout, under CI (any value
except "false" or "0"), when the existing agent detection finds an agent, or
with HYPERFRAMES_AGENT=1. Color is off when it is true.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cli): spawn preview in the agent-mode test only when the studio packages are built

From source, preview refuses to load until the studio workspace packages are
built, and the CLI test jobs do not build them. preview is still checked for
being defined with the shared factory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): count only agent-run shells as unattended, and exit 2 for missing arguments

isUnattended() used the telemetry fingerprint detectAgentRuntime(), which also
matches a person typing in Cursor's or Windsurf's terminal (TERM_PROGRAM) and
any Replit shell (REPL_ID, REPLIT_USER). It now uses isAgentDriven(), which
keeps only markers an agent sets on the shells it runs: CURSOR_AGENT counts,
TERM_PROGRAM=cursor, TERM_PROGRAM=windsurf and the Replit variables do not.

The hand-written argument checks in lambda, cloudrun, lambda policies,
capture, remove-background, init (--resolution), tts (--lang) and cloud render
(--idempotency-key, --dry-run) exited 1. They are usage errors and now exit 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 06:28:44 +00:00
Miguel Ángel 815ff9ec8d perf(cli): check's craft pass skips clips the runtime has hidden (#6627) 2026-10-28 06:25:14 +00:00
Miguel Ángel bf7ebe961e fix(cli): local servers never bind a port Chromium refuses to load from (#6620) 2026-10-28 05:16:42 +00:00
Miguel Ángel 4f0bdfb520 fix(cli): a new blank project starts on a black frame, not grey (#6614) 2026-10-28 04:45:34 +00:00
32e171ca91 chore(codegen): resync cloud client from EF 354b4cf4 (#6608)
* codegen: resync cloud client from EF 354b4cf4

* chore(codegen): keep page_motion_blur in the generated render request

The CLI already sends page_motion_blur (#6087), and experiment-framework adds
it to the public spec in a pending change. Keep it here so the sync does not
break the CLI typecheck in the meantime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 04:43:30 +00:00
Miguel Ángel 9eaf9efb69 fix(core): a graded clip's canvas no longer comes out blank on a real GPU (#6615)
* fix(core): a graded clip's canvas no longer comes out blank on a real GPU

* chore(core): cut the comments the grading fix added

* fix(core): a clip that outgrows the GPU's buffer goes back to its source video
2026-10-28 04:14:32 +00:00
kevin c8052fbdae fix(producer): isPathInside accepts children whose names start with two dots (#5834) 2026-10-28 01:31:40 +00:00
Miguel Ángel e5b4f2bf78 chore: release v0.8.210 (#6600) 2026-10-28 00:49:27 +00:00
Miguel Ángel 439d685678 fix(cli): a page whose renderer crashes fails at once, named as the crash (#6598)
* fix(cli): a page whose renderer crashes fails at once, named as the crash

* fix(cli): keep the crash naming inside the shared error formatter
2026-10-27 23:22:08 +00:00
Miguel Ángel 313792408d fix(producer): HDR sources are tone-mapped for formats that cannot carry HDR (#6597)
* fix(producer): HDR sources are tone-mapped for formats that cannot carry HDR

* refactor(producer): one owner for which output formats can carry HDR

* refactor(producer): drop comments the helper names already carry

* fix(engine): SDR clips are not lifted to HDR when the render tone-maps to SDR
2026-10-27 23:09:15 +00:00
Miguel Ángel 3b2058a2b7 fix(engine): extraction waits out the decode up to a deep seek in a long-GOP video (#6583)
* fix(engine): extraction waits out the decode up to a deep seek in a long-GOP video

* chore(engine): shorten the keyframe gap doc comment

* fix(engine): cancelling an extraction during the keyframe probe reports a cancel

* fix(engine): a segment stopped by another's failure reads as stopped, not cancelled
2026-10-27 20:09:05 +00:00
Miguel Ángel bff39004b0 fix(preview): the proxy copy for the video at the playhead is made ahead of the rest (#6586)
* fix(preview): the proxy copy for the video at the playhead is made ahead of the rest

* test(preview): the resolveProxy assertions carry the priority option

* fix(preview): the proxy copy asked for last goes first, and a repeat ask moves it to the front

* refactor(preview): the proxy wait loop moves its header and progress handling into helpers
2026-10-27 20:08:57 +00:00
Ular Kimsanov 6ae7c7688c docs: links point at hyperframes.studio, the app's new home (#6585)
* docs: links point at hyperframes.studio, the app's new home

* docs: the registry guide publishes to hyperframes.studio

* chore: format README table, regenerate the skills manifest

* docs: leave the browser editor's card wording for its own change
2026-10-27 20:08:22 +00:00
Miguel Ángel 957df5c268 feat(cli): render --prores-profile picks ProRes 4444, hq, 422, lt or proxy for MOV (#6581)
* feat(cli): render --prores-profile picks ProRes 4444, hq, 422, lt or proxy for MOV

* fix(cli): drop a comment line that raised the comment share

* fix(cli): the render plan args type carries --prores-profile
2026-10-27 19:28:49 +00:00
Miguel Ángel e21fd123fd fix(cli): hyperframes add keeps going when Windows holds the file it replaces (#6576)
Publishing a registry file (every block file and hyperframes.lock.json) renamed
its staged copy over the destination with a plain rename, which Windows refuses
with EPERM while antivirus or a just-exited process still holds the file. It now
uses the shared rename that waits and retries on those errors.
2026-10-27 19:09:48 +00:00
Miguel Ángel 0a703edc6f fix(cli): a killed remove-background run no longer leaves its CoreML temp files behind (#6582)
* fix(cli): a killed remove-background run no longer leaves its CoreML temp files behind

On Apple Silicon, CoreML writes about 340 MB of compiled models to the system
temp folder for every session and removes them only when the session is
released. A run stopped by a signal never released it, so every cancelled run
left them. A signal now stops the run through the CLI's shutdown, which
releases the session, and the next run removes what a killed run listed in its
own manifest.

* style(cli): wrap the CoreML shutdown comment to the line limit
2026-10-27 18:29:22 +00:00
Akshar Patel 1d4aadc1fb fix(engine): mux uppercase WebM exports with Opus audio (#6579)
* fix(engine): mux uppercase WebM exports with Opus audio

* test(engine): terminate ffprobe options before input
2026-10-27 18:15:39 +00:00
James RussoandClaude Opus 5.5 07b0f2cda1 fix(producer): stop HDR video windows after the first from repeating one frame (#6575)
* fix(producer): stop HDR video windows after the first from repeating one frame

A raw HDR window seeks to a whole second, then trims its lead-in frames.
trim keeps their timestamps, and the raw muxer writes none, so when the
input has an audio track FFmpeg's default CFR sync pads from pts 0 with
copies of the window's first frame and -frames:v cuts the real ones off.
Phone footage always has audio, so HLG clips rendered at ~half their frames.
Pass frames through as the filter graph emits them.

Fixes #6556

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(producer): hold the first frame until a delayed HDR video starts

Window slots counted from the first decoded frame, so a video stream that
starts after its audio played early by its lead. Slots now count from the
extraction start and show the first frame until the stream begins, as they
did before windowed extraction (#6304) and as Chrome does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(producer): add an HDR fixture whose clips carry audio and span extraction windows

Neither HDR fixture had an audio track, which is how #6556 shipped. Window A
checks frames after the first extraction window are not repeats; window B
checks a video stream that starts 0.5 s after its audio holds its first frame
until the picture starts. The baseline was generated in Docker and the fixture
fails against main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 17:38:34 +00:00
Miguel Ángel 2a0fa7f4de fix(cli): check fails a full-frame element that covers every other clip, naming it (#6572)
* fix(cli): check fails a full-frame element that covers every other clip, naming it

* test(cli): shorten the cover scene helper's comment

* chore(skills): refresh the manifest for the check reference

* fix(cli): full-frame b-roll over a backdrop is not a covered canvas
2026-10-27 17:38:21 +00:00
Miguel Ángel 696ca8f779 fix(cli): a failed --against reference no longer also says it has no frame (#6574) 2026-10-27 16:49:16 +00:00