Commit Graph
3004 Commits
Author SHA1 Message Date
Miguel Ángel bce2140ff2 test(studio): relax large fixture timeout (#3040) 2026-08-04 18:58:24 -07:00
James Russo bbdfee1166 fix(engine): validate remote download integrity (#2938) 2026-08-04 18:02:01 -07:00
Vance IngallsandClaude Opus 5 f3689c1481 fix(producer): scope and harden the markup-payload sniff
Review follow-up on the STUDIO-5433 defense.

Correctness

- The sniff ran above the documented video/audio failure split, so an
  <audio> src that resolved to an HTML payload aborted the whole render
  instead of degrading to duration 0. It now runs inside the same try, so
  video surfaces the typed error while audio still drops out, with a
  warning naming the element.
- Raw fs errors (EISDIR on a directory src, EACCES, the existsSync->open
  ENOENT race, EMFILE) escaped and failed the compile with an unclassified
  error carrying an unredacted temp path. The sniff is now a classifier that
  never throws: an unreadable file reports "not markup" and the real probe
  produces the real error.
- Elements whose duration the compiler never resolves (a data-end video, a
  looping audio) skipped the sniff entirely, so the original ffprobe error
  still escaped, and looping audio was reported as owner "system" after
  every frame had been captured. Video is now caught in the asset preflight,
  which sees every local src regardless of authored timing; audio is
  classified per-element in audioMixer as source/invalid_media/owner "user",
  keeping audio failures non-fatal as they already were.
- Detection is a byte-level check for a leading "<" (BOM-, whitespace- and
  NUL-tolerant, looped read) instead of a <!doctype|<html|<?xml string
  prefix, which missed a NUL-prefixed payload, >256B of leading whitespace,
  UTF-16-encoded HTML, and a prolog-less <svg. No supported container starts
  with "<", so the allowlist no longer grows per payload shape.
- finally { await fh.close() } could replace the in-flight typed error with
  the close error.

Routing and privacy

- MARKUP_NOT_MEDIA is now in SAFE_RENDER_ERROR_CODES, the Lambda terminal
  name map, the CDK and SAM non-retryable plan lists, and the Cloud Run
  non-retryable set, and the class carries owner/retryable. Previously the
  API emitted errorCode: undefined and a deterministic authoring bug burned
  the full distributed retry budget.
- The message no longer carries 32 raw payload bytes or the src.
  redactTelemetryString preserves host and path for HTTP srcs, so
  per-tenant CDN paths reached a message the server forwards to clients.
  Correlation is a sha256 element fingerprint, matching
  AssetMediaTypeMismatchError.
- The message names both causes (unresolved nested-composition URL, or an
  HTML/XML error page served as 200) rather than misdiagnosing an S3 403
  body as an authoring bug.

Tests

- Byte-level detection is unit-tested in engine: markup shapes, BOMs,
  UTF-16, nine container signatures, unreadable inputs.
- Replaced the tautological assertions. The old checks for "html" in and
  "moov" absent from a fixed message template could not fail for any input.
- New coverage for audio degradation, the audioMixer classification, the
  preflight video/image/audio split, and the API error metadata.
- The sibling htmlCompiler.mediaType failure was a vitest-under-bun runner
  mismatch, not a missing ffmpeg binary. It passes, including the 4-wide
  probe-semaphore invariant the sniff now runs inside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:38:51 -07:00
Miguel Ángel 7bf425b7a9 docs(studio): document timeline keyboard navigation (#3031)
* feat(studio): expose timeline treegrid semantics

* feat(studio): coordinate logical timeline focus

* feat(studio): add timeline keyboard controls

* docs(studio): document timeline keyboard navigation
2026-08-04 17:09:07 -07:00
Miguel Ángel 0f1333c80d feat(studio): model logical timeline navigation (#2712) 2026-08-04 16:48:39 -07:00
Miguel Ángel 19b8a1f3c5 refactor(studio): extract live timeline clock (#2711) 2026-08-04 16:27:41 -07:00
Miguel Ángel ebdd1893c4 fix(studio): reconcile external edits before reload (#2993)
* fix(studio): reconcile external edits before reload

* fix(ci): retry transient workspace installs

Make external reload retry behavior honest and isolate reload listeners.

Remove the dead SDK timestamp parameter.
2026-08-04 23:13:19 +00:00
Miguel Ángel 8a0dccfc72 feat(studio): add external conflict recovery UI (#2992) 2026-08-04 22:43:20 +00:00
Vance Ingalls (via Via) 8a11e9776d feat(producer): sniff HTML payload before ffprobe in resolveMediaDuration
STUDIO-5433 defense: when the downloaded media file begins with
<!DOCTYPE, <html, or <?xml, throw a typed HtmlNotVideoError naming
the offending src instead of letting ffprobe emit an inscrutable
moov-atom-not-found on a plain HTML page.

Complements #3033 diagnosability layer. Root-cause EF fix ships
separately.

Signed-off-by: Via <vance@heygen.com>
2026-08-04 22:42:36 +00:00
Miguel Ángel a99caad581 feat(studio): coordinate external file changes (#2991) 2026-08-04 22:20:44 +00:00
Miguel Ángel b30a23402e feat(studio): preserve external file conflicts (#2990)
* fix(studio): drain pending edits before reload

* fix(studio): address drain review feedback (#2989)

- prioritize conflicts and clear recovered DOM queue errors
- cover delayed blur effects and missing drain branches
- document stacked consumers and extend write-token retention

* test(studio): satisfy drain audit gate (#2989)

- share the editor-save hook harness across drain regressions
- extract settled failure inspection from the drain loop

* feat(studio): preserve external file conflicts

* fix(studio): isolate retry write receipts

* test(studio): cover external conflict recovery safety
2026-08-04 21:19:42 +00:00
Miguel Ángel 4713138544 fix(studio): drain pending edits before reload (#2989)
* fix(studio): drain pending edits before reload

* fix(studio): address drain review feedback (#2989)

- prioritize conflicts and clear recovered DOM queue errors
- cover delayed blur effects and missing drain branches
- document stacked consumers and extend write-token retention

* test(studio): satisfy drain audit gate (#2989)

- share the editor-save hook harness across drain regressions
- extract settled failure inspection from the drain loop
2026-08-04 20:58:19 +00:00
Miguel Ángel 552419c52d fix(studio): make inspector commits transactional (#2987)
* fix(studio): make inspector commits transactional

* fix(studio): make inspector persistence atomic

* fix(studio): preserve synchronous gesture semantics
2026-08-04 20:26:43 +00:00
Miguel Ángel cde5bae4c5 test(studio): pin text-field Backspace routing (#2988) 2026-08-04 13:23:25 -07:00
Miguel Ángel 57d0056813 Merge pull request #3029 from heygen-com/fix-browser-gpu-verify
fix(engine): verify explicit browserGpuMode=hardware instead of trusting it
2026-08-04 13:19:51 -07:00
Miguel Angel Simon Sierra f69c4a0e3a fix(engine): distinguish probe failure from a genuinely absent GPU
A probe that could not run is no evidence about the GPU, so pointing the
operator at GPU passthrough hid broken Chrome installs behind a phantom
problem. Carry a cause off the probe and emit the matching remediation.

Also un-exports buildUnverifiedHardwareGpuWarning (Fallow: engine test
files are not audit entry points, so a test-only import would not have
counted as a consumer) and covers the non-linux branch via the spy.
2026-08-04 13:14:23 -07:00
Miguel Ángel cf45c98454 fix(studio): respect GSAP transform ownership (#2986)
* fix(studio): respect GSAP transform ownership

* fix(studio): enforce GSAP edit ownership consistently
2026-08-04 19:08:02 +00:00
Miguel Ángel 532f06158b fix(parsers): preserve safe GSAP helper defaults (#2985)
* fix(parsers): preserve safe GSAP helper defaults

* fix(parsers): make helper inlining call-safe
2026-08-04 19:08:02 +00:00
Miguel Angel Simon Sierra 6703ea7e04 fix(engine): warn once per process about unverified hardware GPU 2026-08-04 11:04:35 -07:00
Miguel Angel Simon Sierra 131780fe96 fix(engine): verify explicit browserGpuMode=hardware instead of trusting it
Chrome's hardware GL args are advisory: with no usable GPU it silently
falls back to software WebGL and the capture runs at CPU speed. Run the
existing WebGL probe for explicit hardware mode too and warn loudly with
the platform's remediation. The requested mode is still honoured.

Fixes #2967
2026-08-04 11:01:16 -07:00
Vance Ingalls f9ec93459f Merge pull request #2945 from heygen-com/ffprobe-6-argv-sweep
fix(cli,core,lint,producer,studio-server): terminate ffprobe options everywhere, pin the contract
2026-08-04 10:43:14 -07:00
Vance IngallsandClaude Opus 5 1664fe6ad7 fix(core,producer,skills): unicode paths, non-Error rejections, shell callers
Three R3 findings.

The redactor's segment classes were ASCII `\w`, so `/数据/客户/秘密视频.mp4` and
`/data/客户/secret.mp4` went out verbatim — and the generic redactor also feeds
CLI telemetry and producer observation messages, where no known-path list
compensates. Segments are now defined by their delimiters instead of an
alphabet, which is correct for every script by construction rather than
requiring Unicode classes to be kept correct. The bare-relative lookbehind had
the same ASCII assumption and let a match start mid-token, redacting
`客户/秘密/视频.mp4` to `客户[path]`; it is now a token boundary, and
bare-relative runs before absolute so it claims the whole token.

sanitizeProbeFailure cast the rejection reason to Error and read `.message`.
An injected probe can reject with anything, so `Promise.reject("failed")` gave
`undefined` and threw inside the redactor — converting a returned failure
result into a rejected promise. Normalized at the boundary, and
redactKnownPaths no longer throws on a non-string.

The contract only admitted .ts/.js/.mjs/.cjs, so it missed shipped shell and
Python callers. frame_strip.sh passed a user-controlled path as ffprobe's last
positional with no terminator; render-and-composite.sh had four more. Both
fixed, and the sweep now covers .py/.sh. Python list argvs are bracket
literals so they get the same position check; shell command lines get a
separate presence check, because checking position there needs a shell parser
— stated as the weaker guarantee it is rather than implied to be equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:18:21 -07:00
Vance IngallsandClaude Opus 5 6c5403f7cd fix(producer): drop the ReDoS-prone literal-argv regex for a linear scan
CodeQL flagged js/redos on the all-literal argv matcher. It was right: the
`(?:"[^"]*"\s*,?\s*)+` form nests a quantifier inside a quantifier with an
optional separator, so whitespace can be matched two ways and a long
non-matching argv backtracks exponentially.

Replaced with a linear scan — find the spawn head, slice to the closing
bracket, and check the entries — plus small named helpers. Same behaviour: an
all-literal argv is treated as taking no input, an argv with a bare identifier
still has to be understood (verified by adding one and watching the guard fail).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:41:39 -07:00
Vance IngallsandClaude Opus 5 c81f68b592 test(producer): treat an all-literal probe argv as taking no input
CI runs the PR merged with main, so it saw a caller my branch predated:
`spawnSync("ffprobe", ["-version"])` in engine/src/utils/ffprobe.test.ts. That
is a capability check with no runtime path, so there is nothing to terminate,
but the unclassified guard flagged it as a caller it could not parse.

An argv whose entries are all string literals carries no input by
construction. Those are dropped before the check; an argv with a bare
identifier still has to be understood, verified by adding one and watching the
guard fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:29:19 -07:00
Vance IngallsandClaude Opus 5 255cf92915 fix(skills,producer): terminate ffprobe options in shipped skill scripts
The contract test only walked packages/*/src and only .ts, so it could not see
the shipped agent tools under skills/**, which are .mjs/.cjs. 19 call sites
there and in package tests were still missing `--` immediately before the
input while the suite reported the bug class closed — a dash-prefixed filename
is parsed as an option and fails the same way.

Sweeps packages/, skills/ and scripts/ now, including .mjs/.cjs and test
files (dither.test.mjs was one of the broken sites). Excludes only the
contract test itself, which documents the contract with example argvs
including a deliberately misordered one.

Two guards were fixed while widening: the terminator must never be inserted
after `-i`, which consumes the next token (a blind pass hit an ffmpeg input
and a base64 -i), and comment prose describing a spawn is not a spawn.

Also routes every audioPadTrim probe failure through one sanitizer at the
boundary. runFfprobeJson scrubbed its own stderr, but
defaultProbeVideoFrameInfo threw `no video stream in ${videoPath}` raw into
the public PadTrimAudioResult.error, and an injected probe can throw anything.
The redaction unit tests all passed with the caller wiring deleted; the new
public-path regressions fail without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 e79ab3ab31 fix(core,producer): redact bare relative paths and the known input path
The generic scrub still missed a relative path with no `./` prefix:
`customer/acme-secret/video.mp4` and `assets/bgm.mp3` reached telemetry
completely unredacted, because the absolute rule needs a leading slash and
the `./` rule needs the dot. Adds a rule for them that still leaves `N/A`,
`24/1` and `48000/1001` alone.

Shape matching is a net with holes by construction, so audioPadTrim now
also redacts the exact path it put in the argv, plus its basename, before
the generic scrub runs. It built the argv, so it does not have to guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 d04569e37f fix(core): redact any path in telemetry, not an allowlist of roots
redactTelemetryString enumerated roots — /Users, /home, /opt, /tmp and a
handful more — so a project on /data, /Volumes, an NFS mount or any root a
user invented reached telemetry verbatim. Relative paths and bare basenames
were never redacted at all, and audioPadTrim routes raw ffprobe stderr
through this on every probe failure.

Now redacts by shape: absolute paths under any root (two or more segments,
so N/A and a 24/1 frame rate are not mistaken for one), relative paths
including dash-prefixed ones, and bare basenames with an asset extension.
URLs still keep their host and drop only the query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:35 -07:00
Vance IngallsandClaude Opus 5 91a7cb1f5b test(producer): discover ffprobe callers and pin terminator position
The prior contract test scanned a hardcoded file list for a format flag
followed by a bare identifier, so it only matched the shape it was written
against. Mutation testing showed removing `--` from engine/utils/ffprobe.ts
and cli/commands/init.ts did not fail it.

Now walks packages/*/src and finds callers itself, checks that `--` is
immediately BEFORE the input rather than merely present, and compares
discovery against a manifest so a regex regression cannot silently stop
checking a known caller. A separate guard fails on any file that spawns a
probe binary but builds an argv this test cannot parse.

All 11 seams mutation-tested for both removal and misordering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -07:00
Vance IngallsandClaude Opus 5 d0dbf11ef5 fix(producer,studio-server): finish the ffprobe argv sweep, pin the contract
The previous commit claimed "all nine now terminate their options". That
was false: `producer/src/utils/audioRegression.ts:307` still passed the
path bare, and it is production source used by the regression harness.
A repo-wide audit found two more in studio-server
(`mediaValidation.ts`, `mediaMetadata.ts`) — their current callers pass
absolute paths, so they were defence-in-depth rather than live bugs, but
the exhaustiveness claim should be true rather than narrowed.

Eleven sites total, all terminated.

Adds a SOURCE-level contract test, which is the gap that let this
happen twice. #2740 fixed one of ten sites and shipped a regression
asserting the argv of that single site, so CI reported the class closed
while nine invocations still parsed `-intro.mp4` as an option. A
per-site unit test has the same blind spot for site twelve; scanning the
tree does not. The test also asserts its own coverage list has not
shrunk.

Verification: engine 1300, lint 511, core 1431, studio-server 398, cli
init/webmAlphaCheck/whisper 146, producer utils 51, audioPadTrim 18.
Removing any single terminator fails the contract test by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -07:00
Vance IngallsandClaude Opus 5 47564ab94c fix(cli,core,lint,producer): terminate ffprobe options at every call site
#2740 added `--` to one of nine independent ffprobe invocations, so the
bug class it closed stayed open everywhere else while CI reported it
fixed — the regression test asserts the argv of that single site.

Reproduced on ffprobe 8.1.1: an asset named `-intro.mp4` probes fine
through extractMediaMetadata but fails with "Missing argument for option
'intro.mp4'" in audio pad/trim (mid-render), `hyperframes init`, whisper
duration probing and webmAlphaCheck. hevcPreviewLint catches and returns
false, so a dash-prefixed HEVC preview silently passes the lint rule.

Terminated at all of them:
  producer/services/render/audioPadTrim.ts (x2)
  producer/plan-parity-analysis.ts
  cli/commands/init.ts
  cli/utils/webmAlphaCheck.ts
  cli/whisper/transcribe.ts (x2)
  core/mediaGradeAnalyzer.ts
  lint/hevcPreviewLint.ts

audioPadTrim's runFfprobeJson is a near-verbatim clone of the engine's
runFfprobe and structurally cannot add the terminator itself, because
callers bake the input path into `args`. It now asserts the terminator
is present rather than letting a dash-prefixed path through, takes the
same stdio ["ignore", ...] as the engine helper, and redacts its stderr
— it was throwing raw ffprobe output, which echoes the input path, into
logs and telemetry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 02:23:34 -07:00
Vance Ingalls df8b9604fe chore: release v0.7.92 2026-08-04 01:49:05 -07:00
Miguel Ángel 0a70ba6717 fix(studio): scope timeline ease focus lifecycle (#2710) 2026-08-03 23:06:46 -07:00
Vance Ingalls 8c6cf90ff9 chore: release v0.7.91 2026-08-03 22:48:00 -07:00
Vance Ingalls 71fd96bbf1 Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment
2026-08-03 22:35:37 -07:00
Miguel Ángel 677fd31e9a perf(studio): stabilize virtualized beat gestures (#2709) 2026-08-03 21:35:40 -07:00
Miguel Ángel 423c5ffadb fix(studio): scope timeline context targets (#2708) 2026-08-03 21:19:49 -07:00
Miguel Ángel 4e7fcf7f2a fix(core): resolve sub-composition sibling asset paths everywhere (#2994)
Extends the studio-preview fix to the render path and the asset-discovery
utilities, which share the same resolver and had the same defect.

`rewriteAssetPath` takes an optional `assetExists` probe. A plain relative ref
authored in a sub-composition (`_shared.css`, `clip.mp4`) is re-pointed at the
composition's own directory when that sibling exists on disk; project-root refs
with no sibling (the registry's `assets/logo.png` convention) stay as authored.
Callers that can see the filesystem supply the probe, so the module stays free
of node:fs.

Also fixes a second defect in the inliner: `<head>` <link> hrefs and external
script srcs are hoisted into the root document but never went through the
rewrite at all, so even the documented `../` form escaped the project and 404'd
at render time.

Wired into the preview bundler, the producer compiler, the studio preview
builder, the HEVC preview lint, the project lint's asset scans, publish proxy
baking, and media-treatment source resolution.
2026-08-03 21:13:35 -07:00
Miguel Ángel 91d14744a0 perf(studio): virtualize timeline marquee selection (#2707) 2026-08-04 06:03:21 +02:00
Miguel Ángel bb5c603870 perf(studio): stabilize virtualized timeline drops (#2706) 2026-08-04 05:45:45 +02:00
Miguel Ángel 44bee4c3cf perf(studio): stabilize virtualized keyframe retiming (#2705) 2026-08-04 05:14:09 +02:00
Miguel Ángel f52398ceef perf(studio): stabilize virtualized clip gestures (#2704) 2026-08-04 04:58:15 +02:00
Miguel Ángel cbd8a77d16 fix(studio): resolve sibling asset paths in sub-composition previews (#2983)
Fixes #2956

## What

A composition in a subdirectory that references a **sibling** file (`<link rel="stylesheet" href="_shared.css">`, not `../_shared.css`) is now resolved against the composition's own directory when building the standalone sub-composition preview page.

## Why

The preview page borrows the project-root `<base href="/api/projects/:id/preview/">`, but the path rewriter only rewrote `../`-prefixed paths. So `design/styleframes/frame-01.html` referencing `_shared.css` was served unrewritten and the browser requested `/preview/_shared.css` → **404**.

With its stylesheet missing, the frame renders unstyled: `body` has no background, and the thumbnail generator's transparent-body fallback paints it `#1c2028`. Result: dark navy thumbnail with unreadable dark text on every styleframe in the Board view.

The report attributed this to project scale (~21 sibling files). It is not scale-related: a 2-file project reproduces identically, and the same file moved to the project root renders correctly. The trigger is **composition-in-a-subdirectory + relative sibling asset ref**.

Reproduced before the fix (single `curl` against the thumbnail endpoint, plus a direct headless capture of the preview URL):

```
HTTP 404 http://localhost:5190/api/projects/big/preview/_shared.css
body bg: rgb(28, 32, 40)
```

## How

`resolvePreviewAssetPath` in `packages/studio-server/src/helpers/subComposition.ts`, applied through the single rewrite pass all three dispatch branches (template / full-doc / fragment) already share, so `src`, `href`, inline `style` urls, and `<style>` blocks all get the same rule:

1. `../` paths keep resolving against the composition dir (unchanged, shared with the producer's inliner so preview and render agree).
2. Any other relative path is re-pointed at the composition's directory **only when that sibling file exists on disk**.

The disk check is what keeps the two conventions apart: registry blocks are installed into a subdirectory but reference project-root assets (`assets/logo.png`), which are already correct under the root base and have no sibling on disk, so they are left untouched.

Not changed: the `#1c2028` transparent-body fallback in the thumbnail generator. It is correct for genuinely transparent compositions; the illegibility was a downstream symptom of the 404.

## Test plan

- [x] Unit tests added/updated — two tests in `subComposition.test.ts`: a red-first regression guard for the sibling `<link>` / `<img>` / `url()` case, and a guard that project-root-relative refs with no sibling on disk stay untouched.
- [x] Manual testing performed — reproduced the dark thumbnail on a generated project (21-file and 2-file variants both reproduce), then confirmed the same URL renders the white-to-lavender gradient with legible text after the fix, with no 404 in the network log.
- [x] `packages/studio-server` suite green: 29 files / 402 tests. Lint, format, typecheck clean.
- [ ] Documentation updated — n/a
2026-08-04 04:09:50 +02:00
Xinyu YANG 70cc4f59a1 fix(cli): stop bare --frame-check from swallowing the next flag (#2966)
citty parses string options greedily, so a bare --frame-check consumed
the following flag as its value (--caption-zone silently dropped,
--json disabled) and failed with an error blaming --caption-zone.

Normalize rawArgs at the check command boundary (bare --frame-check
followed by a flag or at end becomes --frame-check=), attribute
frame-check spec errors to --frame-check, and detect dash-prefixed
values with corrective guidance.

Fixes #2965
2026-08-04 04:03:04 +02:00
James Russo 127eb19371 fix(producer): correct short VFR frame coverage (#2936)
* fix(producer): correct short VFR frame coverage

* fix(engine): keep VFR extraction seek-local

* fix(engine): match ffmpeg decimal frame boundaries

* fix(engine): preserve exact extraction frame rates

* fix(engine): key frame cache by exact rate
2026-08-03 18:42:03 -07:00
James Russo 9792c32950 fix(producer): reject asset media type mismatches (#2937)
* fix(producer): reject asset media type mismatches

* fix(engine): document read-only AVIF probe

* fix(engine): bound read-only AVIF brand probe

* fix(producer): make media preflight lifecycle-safe

* fix(producer): reconcile runtime media before preflight

* fix(engine): avoid writable file-open detection

* fix(producer): close runtime media preflight gaps
2026-08-03 18:16:41 -07:00
WaterrrForever bc6dbc7e21 fix(cli): stop dropping queued telemetry when process.exit races the final flush (#2970)
* fix(cli): stop dropping queued telemetry when process.exit races the final flush

Two exit-path defects introduced by the 0.7.65 process-lifecycle refactor:

1. The 'exit' handler returned early once finalizeCli had started, which
   also skipped the flushSync() fallback. When an agent-pipe EPIPE killed
   the process mid-flush (the NORMAL teardown under Claude Code / Codex),
   the still-queued render_complete was silently dropped — fleet delivery
   fell from ~90% (0.7.55-0.7.64) to ~35%. flushSync() is now
   unconditional: empty queue is a no-op, event uuids dedupe re-sends.

2. The EPIPE handlers set commandFailed unconditionally, so every piped
   successful render scored success:false in cli_command_result (fleet
   success rate collapsed 89% -> 5-25%). EPIPE now only marks failure
   when the pipe died before the render artifact was validated, matching
   the existing isRenderSucceeded() exemption on the uncaughtException
   path.

Regression tests cover both: flushSync-after-finalize, and EPIPE
before/after artifact validation.

* fix(cli): don't score a validated render as failed due to pre-artifact noise

Review follow-up: commandFailed can be set by noise that precedes artifact
validation — a stray unhandledRejection mid-render, or an EPIPE firing
before markRenderSucceeded on a run that still completes. Once the
artifact validates, that earlier noise must not flip the run's
cli_command_result to success:false. Genuine failures keep a non-zero
exit code and are still caught by the exitCode check.

Extracted commandSucceededForTelemetry() and applied it at both tracking
sites (finalizeCli and the exit handler), with a regression test.

* test(cli): pin the production-reachable producer of the stale-failure override

Review note: the pre-artifact-noise test drives the scenario with an
EPIPE, which only reaches 'render validates afterwards' because
process.exit is mocked — that sequence can't occur in production. Add a
test for the reachable producer: an unhandledRejection before validation
(the handler deliberately does not exit), followed by a validated render,
must score success:true at exit code 0. Verified red on the pre-override
cli.ts.
2026-08-04 02:02:55 +08:00
James Russo 1e51eaec2c chore: release v0.7.90 (#2958) 2026-08-02 21:57:19 -07:00
James Russo 2339757377 fix: bound HDR and video extraction resources (#2955)
* fix: bound HDR and video extraction resources

* fix: trim negative video extraction preroll

* fix: skip invisible video extraction windows

* fix: preserve negative-start loop and held tails

* fix: cap finite video slots to source duration

* fix: bound held-tail frame extraction

* fix: plan from playable video duration

* fix: preserve open-ended held video tails

* fix: resolve held tails from decoded frames

* fix: normalize final-frame probe timestamps

* fix: handle unseekable final-frame sources

* fix: dedupe final-frame probes per render

* refactor: clarify output dynamic range contract
2026-08-02 20:21:54 -07:00
Miguel Ángel 411ada0d90 chore: release v0.7.89 (#2953) 2026-08-02 20:25:14 +02:00
Miguel Ángel 59fe32b633 Merge pull request #2952 from heygen-com/fix/2951-ignore-generated-cache-watcher
fix(cli): ignore generated caches in project watcher
2026-08-02 19:51:43 +02:00