Commit Graph
100 Commits
Author SHA1 Message Date
Vance Ingalls c830aa83c5 chore: release v0.7.55 2026-07-12 11:52:53 -07:00
Vance Ingalls 27eec5be6c Merge pull request #2301 from heygen-com/07-12-feedback-skill-bug-repro-guidance
docs(skills): tell agents to file reproducible render bugs, not paraphrases
2026-07-12 11:51:10 -07:00
Vance Ingalls 2a0c603430 Merge pull request #2300 from heygen-com/07-12-fix-software-video-beyond-viewport
fix(producer): stop clipping the bottom edge of software-GPU video renders
2026-07-12 11:50:56 -07:00
Vance IngallsandClaude Opus 4.8 6a3da1f624 docs(skills): tell agents to file reproducible render bugs, not paraphrases
The `hyperframes feedback` convention only prompted for a free-text
`--comment` "with the failing composition pattern and what you tried".
Agents dutifully filed vague reports (blank CJK text, mid-run exit, 4K
timeout) with no error string, no failure-mode, and — critically — no
published composition, so none could be reproduced or root-caused.

Two additions to the CLI skill:
- Lead bug reports with `--file-issue` (+ `--dir`), which publishes a
  minimal repro of the project to a public URL. A comment alone almost
  never lets a maintainer reproduce; the composition is what does.
- Give the `--comment` a concrete bug checklist: exact error string
  verbatim + whether output was produced / fell back / hard-exited; the
  isolated trigger; exact command + HF_*/PRODUCER_* env; frame/timestamp +
  visual defect. Drop the "repeat env" ask (the CLI already attaches it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:28:09 -07:00
Vance IngallsandClaude Opus 4.8 de95daa096 fix(producer): stop clipping the bottom edge of software-GPU video renders
resolveVideoCaptureBeyondViewport gated Chrome's beyond-viewport screenshot
path to hardware-GPU captures, to skip the full-surface software
re-rasterization tax. But without beyond-viewport, the viewport-bound
capture clips the bottom edge of any frame containing a native video
surface (the same #1094 tall-portrait guard the alpha capture paths already
hardcode) — leaving ~87 bottom rows black.

This hit two cohorts: software-GPU macOS/Linux hosts, and — worse — EVERY
distributed chunk render, which hardcodes browserGpuMode "software", so the
whole distributed fleet shipped video renders with a black bottom band.
Reporter confirmed forcing resolveVideoCaptureBeyondViewport=true fixes it.

Correct output wins over the software perf optimization: enable
beyond-viewport for any render with a native video surface, regardless of
GPU mode. Drops the now-vestigial browserGpuMode parameter (and its type)
and updates both call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:28:09 -07:00
Vance Ingalls 534286a17b fix(producer): fall back fast when the parallel DE streaming capture stalls (#2298)
## Stack
1. **#2298 — DE router stall watchdog** ← you are here
2. #2300 — video bottom-edge clip
3. #2301 — feedback bug-report guidance

## Problem
The DE parallel router auto-enables the interleaved parallel-**streaming** capture for the ≥24 GB macOS trial cohort. If a worker wedges mid-capture (a hung seek/screenshot at an early frame), the render makes **zero frame progress** yet sits until the per-frame CDP `protocolTimeout` (~5 min) fires before the pinned self-verify fallback can run — a silent multi-minute hang shipped to real users.

Reported: stuck at frame 2/2031 for 6+ min, no fallback, until the user manually set `HF_DE_PARALLEL_ROUTER=false` (71 s clean).

## Fix
Add a no-frame-progress watchdog to the parallel branch of `runCaptureStreamingStage`:

- Ticks off `executeParallelCapture`'s progress callback. If no **new** frame lands within `HF_DE_PARALLEL_STALL_MS` (default **60 s** — well under the 5-min protocol timeout, ≫ the 15–32 ms/frame budget), it fires.
- On trip: aborts the **reorder buffer** (so peer workers parked in `waitForFrame` reject instead of deadlocking the `Promise.all` pool) and aborts the pool via a **separate** `AbortController` linked to the parent abort.
- The parent `abortSignal` stays un-aborted, so the orchestrator reads the failure as a generic `capture_error` (not a cancellation) and re-renders on the pinned screenshot path — the same fallback a verify failure already uses.

## Test
- Watchdog trips on no progress → rethrows a stall error (routes to fallback).
- A genuine parent-abort is **not** relabeled as a stall (stays a cancellation).
2026-07-12 11:25:39 -07:00
Vance IngallsandClaude Opus 4.8 07fb35c375 fix(producer): fall back fast when the parallel DE streaming capture stalls
The DE parallel router auto-enables the interleaved parallel-streaming
capture for the >=24GB macOS trial cohort. If a worker wedges mid-capture
(a hung seek/screenshot at an early frame) the render made no frame
progress yet sat until the per-frame CDP protocolTimeout (~5 min) fired
before the pinned self-verify fallback could run — a silent multi-minute
hang shipped to real users (report: stuck at frame 2/2031 for 6+ min,
no fallback, until HF_DE_PARALLEL_ROUTER=false).

Add a no-frame-progress watchdog to the parallel branch of
runCaptureStreamingStage. It ticks off executeParallelCapture's progress
callback; if no NEW frame lands within HF_DE_PARALLEL_STALL_MS (default
60s, well under protocolTimeout and >> the 15-32ms/frame budget), it
aborts the reorder buffer (unsticking peer workers parked in waitForFrame
so the pool doesn't deadlock) and aborts the pool via a SEPARATE
controller linked to the parent abort. Because the parent abortSignal
stays un-aborted, the orchestrator reads the failure as a generic
capture_error (not a cancellation) and re-renders on the pinned screenshot
path — the same fallback a verify failure already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:35:21 -07:00
Vance Ingalls ae4946e624 chore: release v0.7.54 2026-07-11 17:15:24 -07:00
Vance Ingalls bd86fb803d Merge pull request #2266 from heygen-com/07-11-fix-cli-check-detect-invisible-text
fix(cli): flag text with an effectively transparent fill in check
2026-07-11 17:10:44 -07:00
Vance Ingalls 1a9c1b66b0 Merge pull request #2264 from heygen-com/07-11-fix-cli-localize-remote-fonts-in-audit-capture
fix(cli): localize remote fonts in snapshot/check capture to match render
2026-07-11 17:05:35 -07:00
Vance Ingalls fa902400c9 fix(cli): only exclude clipped text when a background actually paints the glyphs
Addresses review on #2266: the gradient-text exclusion was too broad — any
background-clip:text skipped the invisible-text check, so a broken/missing
gradient (clip:text with no image and a transparent background, which paints
nothing) went unreported. Now exclude only when a real background fills the
glyphs (background-image != none, or an opaque background-color). Expands the
test suite to the reviewer's full case set: direct transparent fill, inherited
transparent fill over an opaque child color, color:transparent fallback, opaque
baseline, gradient-over-real-background exclusion, broken-gradient still flagged,
and empty-text no-op.
2026-07-11 17:04:30 -07:00
Vance Ingalls a8fd8562a8 fix(cli): read effective text fill by property, not getPropertyValue
The layout-audit test mock returns computed styles as plain camelCase
properties (no getPropertyValue), so the invisible-text detector threw
'cs.getPropertyValue is not a function' and broke the whole audit in CI. Read
webkitTextFillColor/webkitBackgroundClip/backgroundClip by property to match
the rest of the script (works in a real browser too). Adds tests: flags
transparent -webkit-text-fill-color, ignores opaque color, ignores gradient
text (background-clip:text).
2026-07-11 17:00:40 -07:00
Vance Ingalls 9ae84c652c fix(cli): flag text with an effectively transparent fill in check
Wild report (5th in cluster, CLI 0.7.53): snapshots omitted all text while
check passed — text painting with a transparent -webkit-text-fill-color (which
overrides `color` for the glyph fill AND inherits, so a parent's transparent
fill silently blanks descendant text that has its own opaque `color`) renders
invisible, but every geometry/occlusion/contrast audit missed it. Contrast in
particular reads `color`, not the fill that actually paints, so white-`color`
+ transparent-fill text scored as high-contrast and passed.

Add an invisible-text detector to the layout audit: flag any text element whose
effective fill (computed -webkit-text-fill-color, which already resolves to
`color` when unset) is transparent. Gradient/clipped text (background-clip:text)
legitimately uses a transparent fill and is excluded. Verified: check now fails
on an inherited-transparent-fill fixture (text_not_painted) while gradient text,
body-inherited color, sub-composition color, and real registry examples stay
clean.
2026-07-11 16:52:24 -07:00
Vance Ingalls fc2b905837 fix(cli): distinguish producer-absent from injector failure in font localization
Addresses review on #2264: the localization helper had one broad catch around
both dynamic producer resolution and injector execution, so it couldn't tell a
benign 'producer not in this environment' from a real injector/fetch failure,
and emitted no diagnostic. Split into loadFontInjector() (returns null when the
module is absent — silent fail-open) and localizeWithProducer() (warns ONCE per
distinct message when the injector itself throws, then fails open). Per-family
resolution failures remain the injector's own responsibility (producer's
warnUnresolvedFonts). The localizer seam is injectable; tests now cover success,
producer-unavailable, injector-throw, warn dedup, and call-site integration.
2026-07-11 16:39:50 -07:00
Vance Ingalls aa10d49234 fix(cli): resolve producer font-localization at runtime so vitest transform doesn't fail
The CLI test job builds with --filter '!@hyperframes/producer', and render.ts
imports producer only as a type — so a static import("@hyperframes/producer")
in the font-localization helper failed Vitest's transform-time module
resolution ("Failed to resolve entry for package"), breaking checkBrowser
tests and the helper's own test. Keep the specifier out of the static module
graph (@vite-ignore + variable specifier) so it resolves at runtime only:
production/installed CLI has producer in node_modules and localizes fonts;
the test env fail-opens to the plain bundle. Localizer is now injectable so
the helper's unit tests cover it without needing producer resolvable.
2026-07-11 16:30:13 -07:00
Vance Ingalls f53a75bfe8 Merge pull request #2224 from heygen-com/parallel-streaming
feat(producer): stream parallel screenshot/beginframe capture to the encoder
2026-07-11 16:15:49 -07:00
Vance Ingalls ad0d2393fe fix(cli): localize remote fonts in snapshot/check capture to match render 2026-07-11 16:14:36 -07:00
vanceingalls 591235f05c feat(producer): emit eligible_off cohort-sizing signal when parallel-stream router is off 2026-07-11 22:38:04 +00:00
vanceingalls 88c21ccd8b feat(cli): capture_parallel_stream telemetry on success and failure paths 2026-07-11 22:37:25 +00:00
Vance Ingalls 35c231e35f Merge pull request #2255 from heygen-com/07-11-fix_cli_surface_video_frame-injection_failures_in_snapshot_output
fix(cli): surface video frame-injection failures in snapshot output
2026-07-11 15:36:41 -07:00
vanceingalls 6d5a7711f3 feat(producer): route eligible parallel screenshot/beginframe renders to streaming 2026-07-11 22:34:46 +00:00
vanceingalls 2416e9bcd5 feat(producer): routing predicate for non-drawelement parallel streaming 2026-07-11 22:34:13 +00:00
Vance Ingalls fecd7dc1d3 Merge pull request #2248 from heygen-com/bf-reuse-telemetry
feat(producer): surface beginframe no-damage reuse counters in perf summary and telemetry
2026-07-11 15:31:58 -07:00
Vance Ingalls 76f4d2e02f Merge pull request #2254 from heygen-com/07-11-fix_shader-transitions_pin_cloned_scene_roots_to_composition_dimensions
fix(shader-transitions): pin cloned scene roots to composition dimensions
2026-07-11 15:25:13 -07:00
Vance Ingalls 28f492df52 Merge pull request #2253 from heygen-com/07-11-fix_cli_fold_canvas_pixel_hashes_into_the_frozen-sweep_fingerprint
fix(cli): fold canvas pixel hashes into the frozen-sweep fingerprint
2026-07-11 15:24:51 -07:00
Vance Ingalls edc0a1624b fix(cli): surface video frame-injection failures in snapshot output 2026-07-11 14:42:44 -07:00
Vance Ingalls b3d851d65a fix(shader-transitions): pin clones to their measured live box, not composition size
Addresses review feedback on #2254: the previous fix hardcoded every clone
to the full composition width/height regardless of the source scene's own
sizing, silently overriding any authored explicit width/height. Measuring
each scene's getBoundingClientRect() while still live in the document (its
inset:0 already resolves correctly against the real ancestor chain there)
and reapplying that exact box to the clone fixes the 0x0 collapse without
ever clobbering an author's own layout.
2026-07-11 14:42:43 -07:00
Vance Ingalls 3338a6f350 fix(cli): fold canvas pixel hashes into the frozen-sweep fingerprint 2026-07-11 14:42:43 -07:00
Vance Ingalls 9e3ff630f4 fix(shader-transitions): pin cloned scene roots to composition dimensions 2026-07-11 14:42:43 -07:00
Vance Ingalls 1dfb016f3b Merge pull request #2256 from heygen-com/07-11-fix_producer_disk-headroom-gate-before-hdr-pre-extraction
fix(producer): disk-headroom gate before hdr raw frame pre-extraction
2026-07-11 14:38:12 -07:00
Vance Ingalls 676af2bd51 Merge pull request #2252 from heygen-com/07-11-feat_cli_preview_--browser-no-gpu_for_gpu-unstable_hosts
feat(cli): preview --browser-no-gpu for gpu-unstable hosts
2026-07-11 14:28:58 -07:00
Vance Ingalls 68690e63ce Merge pull request #2251 from heygen-com/07-11-fix_cli_settle_post-seek_font_subset_loads_before_audit_screenshots
fix(cli): settle post-seek font subset loads before audit screenshots
2026-07-11 14:28:40 -07:00
Vance Ingalls c252a75e22 Merge pull request #2250 from heygen-com/07-11-fix_engine_densify_drawelement_self-verify_with_parallel_worker_count
fix(engine): densify drawelement self-verify with parallel worker count
2026-07-11 14:28:25 -07:00
Vance Ingalls 9582be9804 Merge pull request #2249 from heygen-com/07-11-fix_producer_withhold_de_parallel_router_below_a_machine_ram_floor
fix(producer): withhold de parallel router below a machine ram floor
2026-07-11 14:25:09 -07:00
Vance Ingalls c20e20235b refactor(producer): stop coupling the hdr disk-headroom gate's control flow to error-string matching
Addresses non-blocking review feedback on #2256: the previous shape used
err.message.includes(...) inside a catch to distinguish a real headroom
violation from a statfsSync failure — a future message tweak would silently
fail open (statfs-unsupported and headroom-violation would take the same
code path). Now statfsSync failure returns early (skip the gate) and the
headroom check/throw happens in plain control flow outside any try/catch.
2026-07-11 14:14:09 -07:00
Vance Ingalls 9a1afce43c fix(producer): disk-headroom gate before hdr raw frame pre-extraction 2026-07-11 14:14:09 -07:00
Vance Ingalls 3dc08c0868 feat(cli): preview --browser-no-gpu for gpu-unstable hosts 2026-07-11 14:14:08 -07:00
Vance Ingalls 8bbd5a6496 fix(cli): settle post-seek font subset loads before audit screenshots 2026-07-11 14:14:08 -07:00
Vance Ingalls 1b95af8bbb fix(engine): densify drawelement self-verify with parallel worker count 2026-07-11 14:14:08 -07:00
Vance Ingalls ddcd380a1a fix(producer): withhold de parallel router below a machine ram floor 2026-07-11 14:14:08 -07:00
Vance Ingalls e37ebe7999 fix(producer,engine): gate drawelement on ancestor background-image + tail verify sample (#2247)
## What

Two fixes from adversarial testing of the DE parallel router (10 hostile comps, routed vs screenshot-baseline PSNR). The router itself held — both bugs are in general drawElement fast capture, and one slipped past self-verify.

### 1. Compile gate: ancestor background-image (`producer`)

`drawElementService`'s per-frame ancestor fill replicates what lies behind the captured subtree by walking up the DOM for the nearest non-transparent **`backgroundColor`**. A background-**image** (`linear-gradient`, `url()`) on `body`/`html`/a wrapper reads as transparent in that scan, so a deeper ancestor's solid color paints instead wherever the subtree leaves pixels uncovered.

Measured repro: body `linear-gradient` + html solid color + an element shrinking late in the comp → DE paints the html purple instead of the body gradient. 30.9 dB min frame vs baseline, visually unmistakable. Identical damage single-worker and parallel — general DE bug, in every wild DE render matching this (very common) authoring pattern.

Fix: `detectAncestorBackgroundImage()` in the compiler (DOM-aware — inline styles on the root's ancestor chain + `<style>` rules resolved via `querySelectorAll`, so class-selected wrappers are covered; backgrounds *inside* the root are deliberately not matched). New compile gate `ancestor_background_image`, same shape as the 3D/mix-blend gates, bypass `HF_FAST_CAPTURE_ANCESTOR_BG=true`.

### 2. Self-verify tail sample (`engine`)

The verify grid sampled at `(i+1)/(k+1)` → [20/40/60/80]% of the timeline. The damage above starts at ~79% and peaks after the last sample — verification **passed** on output that bottomed at 30.9 dB (threshold 32 dB would have caught it, it just never looked there).

Fix: `computeDeVerifySampleFractions()` — first k−1 samples evenly spaced, last pinned at 95%. Default k=4 grid becomes [25/50/75/95]%. Kills the whole late-onset damage class, not just this repro.

## Validation

- Repro comp (body gradient + shrink reveal): now gates → baseline route, 54.7 dB avg vs ground truth (was 41.6 avg / 30.9 min with the purple surround)
- Control comp (nested stacked fades, routed): still routes, verify grid `[90, 180, 270, 342] of 360`, passes, 60.6 dB avg — unchanged
- Full adversarial matrix context: 6/10 comps routed clean (49–68 dB min), blend/3D gated correctly, animated-canvas damage caught by verify at 16.5 dB with clean revert, video comps route legitimately (frames pre-extracted)
- Tests: 7 new detection cases (`htmlCompiler.test.ts`), 5 new grid cases (`frameCapture-verifySampleFractions.test.ts`); `compileStage.test.ts` + `frameCapture.test.ts` suites green

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-11 14:13:09 -07:00
vanceingalls 2cd8a750e3 docs(producer): note attempt-scoped beginFrameReuse semantics after partial retries 2026-07-11 20:32:20 +00:00
vanceingalls dde3afb72e feat(producer): surface beginframe no-damage reuse counters in perf summary and telemetry 2026-07-11 19:44:24 +00:00
Vance Ingalls 0809c98518 fix(producer,engine): gate drawelement on ancestor background-image + tail verify sample 2026-07-11 12:00:25 -07:00
Vance Ingalls 7498eb4a3a chore: release v0.7.52 2026-07-10 19:47:59 -07:00
Vance Ingalls 469cfb3a32 Merge pull request #2115 from heygen-com/de-parallel-router-failure-telemetry
feat(producer,cli): full telemetry visibility for DE parallel-router/inversion failures
2026-07-10 18:24:11 -07:00
Vance Ingalls 2d17e34b90 Merge pull request #2189 from heygen-com/fix/sdk-template-gsap-resolver-parity
fix(sdk): include template GSAP scripts in resolver parity
2026-07-10 18:22:34 -07:00
Vance Ingalls 392dd410a5 Merge remote-tracking branch 'origin/main' into de-parallel-router-failure-telemetry
# Conflicts:
#	packages/cli/src/telemetry/config.ts
2026-07-10 18:15:12 -07:00
Vance Ingalls a61f7de8d7 fix(sdk): align template script traversal 2026-07-10 17:36:09 -07:00
Vance IngallsandClaude Opus 4.8 b02703b7c9 fix(cli,producer): opt-in trial polarity + narrowed-fallback-flag docs (review)
Two non-blocking review notes from Rames, both addressed:

1. Trial polarity inverted to OPT-IN: disableDeParallelRouterTrial →
   enableDeParallelRouterTrial. renderLocal is exported, so any programmatic
   consumer (future studio-server path, test harness, distributed runner)
   previously inherited the trial and its process-wide env-var/module-latch
   state without knowing to disable it — and concurrent invocation races
   that state. Now only the CLI's own sequential call sites opt in (the
   single top-level render, and batch at concurrency 1); everyone else gets
   no trial by default. The doc comment names the sequential-invocation
   assumption explicitly.

2. deSelfVerifyFallback semantic narrowing documented at both declarations
   (RenderCaptureObservability + RenderPerfSummary.drawElement): since the
   pinned-fallback retry was widened, the flag means verify-triggered
   SPECIFICALLY — OOM/capture_error fallbacks report false with
   deFallbackReason carrying the reason. Dashboards keyed on
   de_self_verify_fallback=true as "any fallback fired" must migrate to
   de_fallback_reason IS NOT NULL (also called out in the PR body for the
   observability rebuild to pick up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:24:13 -07:00
Vance Ingalls 46602d75f4 fix(sdk): include template GSAP scripts in resolver parity 2026-07-10 16:55:19 -07:00
Vance IngallsandClaude Fable 5 6172d79dc2 fix(cli): atomic config writes, gated trial warning, and write-failure signal
Five findings from a fifth (final scoped) max-effort review of the previous
commit, all local:

1. writeConfig now writes atomically (pid-suffixed temp file + renameSync —
   rename within one directory is atomic on POSIX). This closes the real
   hazard behind the review's torn-read finding: readConfig's corrupted-file
   catch RESETS the config to defaults (telemetry re-enabled, anonymousId
   rotated, trial fields wiped), so a concurrent reader catching a
   non-atomic write mid-flight would silently destroy the user's config —
   and the previous commit's per-render readConfigFresh() at the arm site
   multiplied exposure to exactly that window. Verified against a real
   filesystem, not just the mocked unit tests.

2. writeConfig now returns whether the write landed (errors still swallowed
   — telemetry must never break the CLI). persistDeParallelRouterTrialFired
   uses it to stop immediately on a genuine fs failure (retrying an
   unwritable file is pointless) and reserve its retries for actual
   concurrent clobbers, instead of 3 blind write attempts + 4 disk reads.

3. The persistence-failure console.warn is now !quiet-gated like every
   other trial message — a quiet/batch-json render on an unwritable
   ~/.hyperframes no longer emits unexpected stderr that CI wrappers
   asserting empty stderr would misread as a render failure. The in-process
   latch already guarantees the safety behavior whether or not the warning
   prints.

4. The arm site short-circuits on the in-process fired latch BEFORE the
   fresh config read — post-fired batch rows no longer pay a per-row config
   read + parse + shared-cache invalidation for an answer module state
   already knows.

5. Replaced the new `as T` assertions in render.test.ts's config-state
   factory with an explicitly typed vi.hoisted return (repo TypeScript
   convention: no `as T`).

config.test.ts: node:fs mock gains renameSync (faithful to the new atomic
write); new test covers the success/failure return and asserts no temp file
survives a write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:51:11 -07:00
Vance IngallsandClaude Fable 5 2542e94277 fix(cli): stale-cache arm reads, retry double-count, and unwritable-config re-arm in DE trial
Three root causes from a fourth max-effort review (15 raw findings deduped;
the synthesize step died on a session limit so they arrived unmerged):

1. The previous commit's telemetryEnabled fix was ineffective: the arm site
   passed readConfig() — the process-lifetime cache — into
   isDeParallelRouterTrialBlocked, making it exactly as stale as the
   shouldTrack() memoization it claimed to bypass. A mid-batch
   `hyperframes telemetry off` (or another process persisting fired=true)
   was never observed. Now reads readConfigFresh() at the arm site; the
   test mock previously hid this because readConfig/readConfigFresh were
   behaviorally identical views over one shared object.

2. The verify-and-retry write loop double-counted a render whenever OUR
   write landed but a concurrent writer advanced the file before our
   verify read — the retry re-applied the increment on top (two renders
   → three counts), tripping the 25-render exposure cap early and
   permanently killing the trial with less telemetry than the cap was
   designed to allow. Reworked: the render COUNTER is written exactly
   once, unverified (a lost increment under-counts by one — benign); only
   the FIRED flag is verified and re-asserted, which is idempotent, so
   retries can no longer corrupt anything
   (persistDeParallelRouterTrialFired).

3. writeConfig swallows all fs errors, so on an unwritable ~/.hyperframes
   a reverted outcome could never persist — the trial would re-arm and
   re-fail on every subsequent render forever, silently. Added an
   in-process fired latch (set at decision time, before persistence is
   attempted) consulted by the blocked-check, plus a one-time console
   warning when persistence exhausts its attempts. Later processes still
   re-arm (disk is the only cross-process channel), but each process now
   stops after at most one failure it couldn't record.

Test infrastructure fix enabling all of the above to be tested: the config
mock now models disk vs cache SEPARATELY (readConfig serves the cache,
readConfigFresh re-reads "disk", writeConfig updates both) with a
failWrites hook simulating the real writeConfig's silent error swallowing.
The old single-shared-object mock made cached-vs-fresh mis-routing and
retry iterations untestable by construction.

3 new regression tests: mid-batch opt-out observed through the cache;
fired flag re-asserted after a lost write WITHOUT re-counting the render;
unwritable-config latch blocking re-arm. 56 tests total across
render.test.ts + config.test.ts.

Not fixed (by design): the widened pinned-fallback retry paying a doubled
render on deterministic mid-stream failures (e.g. ENOSPC) — the accepted
tradeoff of the fallback design; cancellation and OOM are special-cased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:59:57 -07:00
Vance Ingalls 6152437d2a chore: release v0.7.49 2026-07-10 09:57:48 -07:00
Vance IngallsandClaude Sonnet 5 dc6df93de5 fix(cli): fix concurrency race, none-vs-undefined bug, and 3 more DE trial gaps
Six findings from a third max-effort code review, focused on the previous
commit's fixes:

1. --batch-concurrency N>=2 runs genuinely concurrent renderLocal() calls
   (Promise.all workers in batchRender.ts), which can't safely share the
   trial's one process-wide env var + module flag — a row finishing first
   could tear down the env var/flag mid-render for a sibling row still in
   flight. Rather than attempt to make shared process-global state safe
   under real concurrency, added RenderOptions.disableDeParallelRouterTrial
   and set it whenever batchConcurrency > 1 — the trial simply isn't
   offered when it can't be evaluated safely.

2. maybeConsumeDeParallelRouterTrial's "outcome === undefined" no-op guard
   almost never fired: aggregateDrawElement (perfSummary.ts) defaults
   parallelRouter to the string "none" for every render, whether or not
   drawElement/the router ever engaged — never undefined. Every ordinary
   render below the router's own frame threshold (the common case) was
   ticking the render-count backstop, tripping
   DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS after 25 completely unrelated
   renders that never touched the router. Now treats "none" the same as
   undefined.

3. isDeParallelRouterTrialBlocked relied solely on shouldTrack(), which
   memoizes its verdict once per process — during a long --batch run, a
   `hyperframes telemetry off` issued from another terminal mid-batch would
   never be observed. Restored a direct config.telemetryEnabled check
   (read fresh every call, unlike shouldTrack()'s cache) alongside it.

4. maybeConsumeDeParallelRouterTrial's config write had no way to detect a
   losing race against a concurrent process — added a verify-and-retry
   loop (write, re-read fresh, retry up to 3x if a concurrent writer
   landed in between) that narrows the window further without a full
   file-locking rewrite.

5. The trial could arm before the first-run telemetry disclosure
   (showTelemetryNotice) was guaranteed to have printed — that notice runs
   via a fire-and-forget, unawaited dynamic import in cli.ts with no
   ordering guarantee relative to the render command. Rather than touch
   that pre-existing async bootstrap chain, gated the trial on
   config.telemetryNoticeShown: it simply never offers itself on a fresh
   install's very first invocation.

6. Added a dedicated config.test.ts exercising readConfig/readConfigFresh/
   writeConfig through the REAL module (node:fs mocked with an in-memory
   fake, not a HOME-env hack) — readConfigFresh's cache-bypass and the
   type-guarded boolean/number parsing had zero coverage through the real
   implementation before this.

Also fixed the test fixture that was supposed to cover finding #2 but used
an unrealistic `drawElement: {}` shape instead of the real
`{ parallelRouter: "none" }` aggregateDrawElement actually produces.

Extracted applyDeParallelRouterOutcome to keep maybeConsumeDeParallelRouterTrial
under the repo's complexity gate after adding the retry loop.

11 new/updated tests in render.test.ts (56 total) + 7 new tests in
config.test.ts. Verified against fallow's audit gate clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 00:31:11 -07:00
Vance IngallsandClaude Sonnet 5 532dad7cc7 fix(cli): fix batch re-entrancy, config race, exposure cap, and shouldTrack gap in DE trial
Four confirmed findings from a max-effort code review of the CLI trial
mechanism:

1. maybeEnableDeParallelRouterTrial's `process.env.HF_DE_PARALLEL_ROUTER
   !== undefined` guard couldn't distinguish "the user set this" from "an
   earlier renderLocal() call in this same process already armed it" — so
   in --batch (all rows share one process), only row 1's outcome could
   ever reach maybeConsumeDeParallelRouterTrial. A revert on any later row
   was silently never persisted. Added a module-level
   deParallelRouterTrialManagedByUs flag to disambiguate, with a test-only
   reset export since it's process-lifetime state a real CLI invocation
   never needs to reset but a test suite sharing one module instance does.

2. writeConfig is a non-atomic whole-file overwrite with no locking, and
   readConfig's cache never invalidates — a concurrently running second
   CLI process (another terminal, a parallel script; doesn't even need to
   be a render, any command calls incrementCommandCount) could silently
   clobber a just-persisted deParallelRouterTrialFired:true with its own
   stale snapshot. Added readConfigFresh (bypasses the cache) and use it
   immediately before the trial's read-modify-write, narrowing the race
   window without a full config-subsystem locking rewrite.

3. The prior commit's semantics flip removed the only exposure cap — a
   healthy router that never reverts now force-enabled the experimental
   path on every eligible render forever. Added
   DE_PARALLEL_ROUTER_TRIAL_MAX_RENDERS (25) as a backstop: the trial turns
   off after this many engaged renders even absent an actual failure.

4. maybeEnableDeParallelRouterTrial only checked config.telemetryEnabled,
   not shouldTrack() — so a dev-mode run or a DO_NOT_TRACK/
   HYPERFRAMES_NO_TELEMETRY user got the experimental path silently armed
   while telemetry was simultaneously blocked underneath it. Now gates on
   shouldTrack() (a strict superset).

Also fixed, lower severity: readConfig's deParallelRouterTrialFired/
deParallelRouterTrialRenderCount parsing now validates the JSON type
explicitly instead of a bare truthy/nullish read, so a hand-edited or
corrupted config can't have the string "false" misread as truthy.

Refactored maybeEnableDeParallelRouterTrial into three smaller functions
(isDeParallelRouterTrialBlocked, stopManagingDeParallelRouterTrial) to
bring cyclomatic/cognitive complexity back under the repo's threshold —
also de-duplicates the "stop managing the env var" logic shared with
maybeConsumeDeParallelRouterTrial.

14 new/updated tests (43 total in render.test.ts), including a direct
regression test for the batch re-entrancy scenario and a loop test for the
render-count cap. Verified the config primitives end-to-end against a real
file, not just the mocked unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 22:55:29 -07:00
Vance Ingalls dda09c8d64 Merge pull request #2110 from heygen-com/fix/prompt-guide-pipeline-fixes
fix(skills): pipeline fixes from prompt-guide validation (BGM, caption accent, voice, PR version)
2026-07-09 22:15:40 -07:00
Vance IngallsandClaude Fable 5 23c9d15b69 fix(skills): address PR #2110 review feedback
- SSOT: the three assemble-index.mjs BGM fallbacks now import
  bgmDefaultVolume() from media-use's bgm.mjs instead of duplicating the
  0.12/0.9 literals (both reviewers). The cross-skill relative import
  matches the existing dependency (each workflow's audio.mjs adapter
  already resolves ../../media-use/audio/scripts/audio.mjs).
- STATUS_ROLE_KEY: extended with info|neutral|alert|caution|critical —
  same hue-carries-meaning class as the original set (all 3 copies).
- bgm.md: phrase the default as bgmDefaultVolume()/BGM_BED_VOLUME with
  "currently 0.12" so the prose survives future tuning.
- fetch-pr.mjs: drop dead mergeCommit field from the gh pr view FIELDS
  list (version resolution uses mergedAt only).
- music-to-video assemble-index.mjs: comment documenting why its BGM
  stays at 0.8 under VO — music is the content there, not a narration
  bed, so the explainer pipelines' 0.12 default deliberately does not
  apply.

Not changed: pickAccent's chroma fallback — both call sites pass keyless
capture palettes (tokens.json hex lists), so no status-role keys exist
to filter on; the keyed preset path goes through semanticColors and the
build-frame remix, which this PR already fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:12:59 -07:00
Vance IngallsandClaude Fable 5 c992a136bf fix(skills): pipeline fixes from prompt-guide validation (BGM, caption accent, voice, PR version)
Behavior fixes surfaced by the prompt-guide validation campaign (Tier 1+2
of the upstream bug list; Tier 3 tracked in #2107). Split out from the
doc-only updates, which follow in a separate PR.

- BGM level: default bed volume under narration was 0.8 linear (~-2 dB,
  ~16 dB too hot vs voice). Now 0.12 (~-18 dB) via shared
  bgmDefaultVolume() in media-use bgm.mjs + assemble-index fallbacks in
  faceless-explainer / pr-to-video / product-launch-video. Explicit
  volume still wins; silent-film 0.9 and music-to-video unchanged.
  Adds bgm.test.mjs (3 cases); bgm.md reference updated to match.
- Caption accent: semanticColors() ranked accents purely by chroma, so a
  preserved status red (#dc2626) outranked the brand accent and captions
  highlighted in error-red. Status-keyed colors now excluded via shared
  STATUS_ROLE_KEY regex consumed by both tokens.mjs and build-frame.mjs
  (all three skill copies kept in sync).
- Voice threading: workflow SKILL.md Step 3.1 blocks now instruct
  choosing the narration voice from the user's ask and passing
  --voice <id>; previously "a male voice" was silently ignored and the
  default (Marcia/am_michael) always won.
- fetch-pr shipping version: MERGED PRs get best-effort shipped_version
  + version_source in pr.json (first release published at/after merge,
  else default-branch package.json marked unreleased); ingest surfaces
  it as a 'Shipped in:' brief line; story-design.md forbids inventing
  versions when absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:12:52 -07:00
Vance IngallsandClaude Sonnet 5 19f90b0b92 fix(cli): keep the DE parallel-router trial on until a real failure, not first engagement
Only consuming telemetry from one data point per install badly undersampled
the "routed" (successful) outcome — the far more common case. Changed
maybeConsumeDeParallelRouterTrial to only turn the trial off when the
router's OWN safety net actually fired (deParallelRouter === "reverted"),
not on a clean "routed" success. This runs the experiment on every eligible
render for an install indefinitely until it hits one real failure, then
stops for that install going forward — trading a slightly higher per-install
ceiling on experimental-path exposure for dramatically more successful-
routing telemetry volume across the fleet.

Also fixed a related edge case while updating this: a render that merely
"routed" (router fired, self-verify never even tripped) but then crashed
for an unrelated reason (e.g. cancellation) no longer counts as a router
failure — only "reverted" (the router's fallback path actually engaged)
does. Cancelling a render isn't evidence the router is unsafe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:49:21 -07:00
Vance IngallsandClaude Sonnet 5 37b6a4e7e5 feat(cli): one-shot DE parallel-router trial per install for real telemetry
HF_DE_PARALLEL_ROUTER is a producer env var with no self-serve opt-in path
for real users, so waiting for someone to manually enable it would never
produce the real-traffic telemetry (revert rate, verify-db distribution)
the router's soak plan calls for.

renderLocal now enables the experiment for free on a fresh install's CLI
renders until it actually engages once (routed or reverted — either
produces telemetry), then persists that to ~/.hyperframes/config.json and
never touches it again for that install. A render whose frame count never
crosses the router's own eligibility threshold doesn't consume the trial —
it stays available for a later render that does qualify.

Never overrides a user's own explicit HF_DE_PARALLEL_ROUTER setting, and
only engages when telemetry is enabled (no point risking the experimental
path if we can't record the resulting signal). Scoped to the in-process CLI
render path only — Docker renders don't thread perfSummary/errorDetails
back to the CLI process, so trial consumption can't be detected there.

Verified the config round-trip against a real file (fresh install ->
undefined -> write true -> persists across reread), not just the mocked
unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:40:16 -07:00
Vance IngallsandClaude Sonnet 5 a355fb2f6b fix(producer,engine,cli): oom wrapping, cancellation, fallback-reason gaps
Three defects found by max-effort code review of this branch:

1. The Bun OOM exact-match regex was defeated by this codebase's own
   parallel-worker error wrapping. executeParallelCapture/formatWorkerFailure
   (parallelCoordinator.ts) always wrap a worker's error as
   "Worker N: <message>", optionally suffixed and joined with other workers'
   segments, all prefixed "[Parallel] Capture failed: ". That wrapping
   defeated the exact-message check for exactly the cohort (deParallelRouter
   routed, N separate Chrome processes) the OOM-drops-to-1 fix targets — a
   real OOM there would retry at the SAME worker count instead of dropping
   to 1. Added a second pattern that recovers the signal by requiring
   "out of memory" appear as the WHOLE content of a "Worker N: ..." segment
   (bounded by end-of-string/"; "), preserving the same exact-match property
   (no bare substring match) while surviving the wrapping. Verified against
   the real wrapping logic, not a hand-typed guess at its shape.

2. shouldRetryViaPinnedFallback didn't exclude cancellation, so aborting a
   render mid-capture on the pinned router/inversion cohort would detour
   through spawning a fresh encoder/capture session before the outer catch's
   RenderCancelledError branch ended the render — delaying "stop" with a
   pointless resource spin-up/tear-down. Added an isCancellation param
   (checked first, before isVerifyError) using the same
   `err instanceof RenderCancelledError || abortSignal?.aborted` check the
   outer catch already uses.

3. deFallbackReason (this PR's new "oom"/"capture_error" values) was set
   locally but never mirrored into RenderCaptureObservability alongside
   deSelfVerifyFallback, so a render that fails AFTER a fallback attempt
   (perfSummary never built) was indistinguishable in render_error telemetry
   from one that never attempted any fallback — undercutting the "how often
   does the OOM retry fire on a render that still ultimately fails"
   question this branch exists to answer. Threaded through
   RenderCaptureObservability → RenderObservabilityTelemetryPayload →
   renderObservabilityTelemetryPayload, mirroring the existing
   deSelfVerifyFallback plumbing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 20:31:24 -07:00
Vance Ingalls a8f242e615 Merge pull request #2112 from heygen-com/vi/figma-scopes-retry
fix(figma): auth/retry/batch hardening, mapper fidelity, skill routing, setup docs
2026-07-09 19:56:52 -07:00
Vance IngallsandClaude Opus ffac2ecbfe docs(figma): lead setup with "which credential do I need", not a token wall
The old setup section was one long token-minting flow with the MCP
connector as an afterthought — but motion/shaders/storyboards need
only the connector (no token at all), and even brand tokens are
easier via MCP on any non-Enterprise plan. A reader wanting only
motion had to wade past REST scope tables meant for a different path.

Restructured into a decision table (what you want → which credential)
followed by two equal, independent steps. Step A's scope list now
states the 3 boxes to check on a normal (non-Enterprise) account
up front, instead of a generic 4-scope table the reader has to
interpret themselves. Step B now states the MCP connector's actual
capability (variable reads work on any plan, rate-limited by tier —
6 calls/month on Starter) instead of "no scopes to configure," which
undersold what it can do.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
2026-07-09 19:41:43 -07:00
Vance IngallsandClaude Opus 49a78a30e4 docs(figma): remove stale duplicate RATE_LIMITED troubleshooting row
Miga's re-review caught it: the new RATE_LIMITED row (client auto-
retries with backoff) landed alongside the old pre-retry row ("wait a
minute and retry; chunk batch renders"), leaving two rows for the same
code — one accurate, one stale. Keep only the current one.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
2026-07-09 19:39:10 -07:00
Vance IngallsandClaude Opus d13c96d470 fix(skills): force figma.com sources through /figma, not raw MCP tools
Two real incidents this week had agents skip /figma entirely and drive
Figma via raw MCP tools (get_metadata/get_screenshot/get_design_context)
when a figma.com URL landed inside a creation-workflow skill. Root cause:
none of the creation workflows mention Figma at all, and the only routing
table that does (/hyperframes) is skipped whenever a workflow is invoked
directly rather than through the entry router — which is the common path.

Going raw loses real infrastructure the CLI/skill guarantees: sanitizeSvg()
before freezing (raw-fetched SVGs are unsanitized), .media/manifest.jsonl
provenance (no cache-hit, no version tracking), and brand-token var()
binding (colors bake as literals, so a later Figma brand change can't
propagate without a full re-import).

Added a "figma source" callout to every creation workflow that could
plausibly receive a figma.com link (product-launch-video, website-to-video,
general-video, motion-graphics, slideshow), plus a defense-in-depth line
in /hyperframes's own routing checklist. The fix lives in the workflows
themselves so it doesn't depend on the entry router being consulted.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
2026-07-09 19:35:22 -07:00
Vance IngallsandClaude Sonnet 5 b3f244a7e9 fix(engine): recognize Bun/JavaScriptCore's OOM message in isMemoryExhaustionError
Found while testing the previous commit's OOM-drops-to-1-worker fallback
end-to-end: the producer's deployed runtime is Bun (JavaScriptCore), not
Node (V8) — see packages/gcp-cloud-run/Dockerfile's `bun dist/server.js`
entrypoint. All 7 MEMORY_EXHAUSTION_ERROR_PATTERNS are V8-specific allocation
failure signatures; JSC's equivalent for the same single-oversized-allocation
RangeErrors is the bare string "Out of memory" (verified against real Bun
behavior), which none of them match. Without this, isMemoryExhaustionError
returns false for genuine production OOM, so the memory-specific worker-count
reduction just added would never actually engage where it's deployed — every
OOM would fall through to the generic capture_error retry path instead.

Matches the FULL (trimmed) message only, not merely a substring — same
rationale as the existing V8 patterns' comment: "out of memory" also appears
in benign WebGL/GPU console noise that must not trip this classifier.

Verified end-to-end from a script inside the producer workspace (importing
the real @hyperframes/engine source, not a stale globally-cached npm dist a
script outside the workspace would otherwise resolve to): a genuine Bun
RangeError from new Uint8Array(Number.MAX_SAFE_INTEGER) now correctly
classifies as memory exhaustion and drives both resolveInversionRetryPlan
and resolveParallelRouterRetryPlan down to workerCount=1 on retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 19:29:14 -07:00
Vance IngallsandClaude Opus c8eff1a4ba fix(core,cli): figma IMAGE fills dropped, rasterize double-paint, tokens false-success
nodeToHtml routed rasterize eligibility off node.type alone, so a
RECTANGLE/FRAME with an IMAGE fill fell through to the generic <div>
path — fillCss() has no IMAGE case, so it rendered an empty box.
IMAGE-filled nodes now route to rasterize like vectors, regardless of
node.type.

Rasterized nodes (vectors, now image fills too) were also getting
their own fill/corner-radius CSS applied on top of the already-
rendered <img> — a flat color block behind/around the real art,
flattening non-rectangular shapes into rounded rects. decorationCss
now skips background and corner-radius/clip for rasterized nodes;
opacity and effects still apply since those aren't baked into the
export.

tokens.ts's styles-fallback path hardcoded entries: [] regardless of
how many published styles were actually found, so the CLI printed
"recorded published style metadata instead" even when styles()
returned zero results. Added styleCount to the result so the message
reflects what happened, and points at the MCP get_variable_defs
fallback when there's nothing to fall back to.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
2026-07-09 19:23:19 -07:00
Vance Ingalls 8c996100cf Merge pull request #2114 from heygen-com/feat/position-edits-render-reapply
fix(core,producer): render SDK position edits in producer pipeline
2026-07-09 19:22:55 -07:00
Vance Ingalls 70ac80b08b fix: clear fallow and windows artifact checks 2026-07-09 18:55:11 -07:00
Vance IngallsandClaude Sonnet 5 51353a7ed6 fix(producer): drop to a single worker on OOM retry instead of the pre-pinned count
shouldRetryViaPinnedFallback retrying OOM was only half the fix: it reused
preInversionWorkerCount/preRouterWorkerCount unmodified, which is
calibration's own pick and can be >= the pinned count that just OOM'd
(calibration wanting 5 while the router pinned to 3). Retrying at equal or
higher parallelism than the failure isn't a remedy — it's the same bet
again, and worsens the odds for this render and anything sharing the host
(PRODUCER_MAX_CONCURRENT_RENDERS runs concurrent jobs in one process).

resolveInversionRetryPlan/resolveParallelRouterRetryPlan now drop to
workerCount=1 specifically when the retry is OOM-triggered — one Chrome
page, the leanest configuration available, not just a different capture
mode at the same worker count. Ordinary self-verify (blank/PSNR) retries
are unaffected — those aren't memory-related, so they keep the
pre-inversion/pre-router count as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 18:52:38 -07:00
Vance Ingalls 960c31f668 fix(core): make generated artifact formatting deterministic 2026-07-09 18:36:22 -07:00
Vance IngallsandClaude Sonnet 5 df57eb0fde fix(producer): widen DE self-verify retry to generic failures on a pinned worker count
The router/inversion pin a fixed worker count regardless of calibration —
exactly the scenario a host-contention timeout or worker crash is most
likely under. Previously only a drawElement self-verify failure (blank
frame / PSNR breach) triggered the existing fallback to the calibrated,
non-DE parallel-screenshot path; any other capture-stage failure on a
pinned render just hard-failed the whole job instead of reusing that same
tested safety net.

shouldRetryViaPinnedFallback widens the retry to any capture failure while
deWorkerInversion="inverted" or deParallelRouter="routed", excluding OOM
(the fallback's worker count can be >= the pinned count, so retrying would
likely just OOM again — fail fast instead).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 18:23:39 -07:00
Vance Ingalls 0971f3b2a7 chore(core): refresh position edits render artifact 2026-07-09 18:21:32 -07:00
Vance Ingalls d2f32c831c fix(core): narrow wrapped seek function type 2026-07-09 18:09:45 -07:00
Vance IngallsandClaude Fable 5 67a4fb6b70 fix(core): use execFileSync for generated-file format step
CodeQL: shell command built from environment values — the oxfmt
invocation interpolated a filesystem-derived absolute path into a shell
string. execFileSync with array args avoids the shell entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 17:52:29 -07:00
Vance Ingalls 68ae4e5160 fix(core,producer): harden render reapply wiring 2026-07-09 17:38:31 -07:00
Vance Ingalls 4e3d639e1a fix(core): preserve position edit fold detection during seeks 2026-07-09 17:38:31 -07:00
Vance Ingalls 85bab88afb fix(core,producer): render SDK position edits in producer pipeline 2026-07-09 17:38:31 -07:00
Vance IngallsandClaude Fable 5 43af582534 docs(figma): sync guide scopes + error table; gate batch line, regen index in finally
Addresses @miguel-heygen's review on #2112:
- BLOCKER: docs/guides/figma.mdx now matches the shipped code/skill —
  adds the Library content: Read-only scope row (+ corrects the
  'falls back, expected' line that was false without it), and the
  troubleshooting table now says bad PATs surface as 403 Invalid token
  (not 401), names the scope in FORBIDDEN, and documents RATE_LIMITED retry.
- nit: the batch summary line no longer claims '1 figma request' when every
  node was a cache hit — says 'all reused from cache — no figma request'.
- nit: index.md regen moved to a finally, so a mid-batch RENDER_FAILED
  leaves index.md consistent with the nodes that did freeze.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:17:39 -07:00
Vance IngallsandClaude Sonnet 5 ec921e143b feat(producer,cli): full telemetry visibility for DE parallel-router/inversion failures
render_error previously carried zero DE-cohort context — a hard failure while
routed (worker crash, OOM, capture timeout from the fixed 3-worker pin
overriding calibration) was indistinguishable from any other failure. The
data existed (RenderCaptureObservability is mutated live and survives into
job.errorDetails on the failure path) but was never projected into the
render_error payload, which only ever drew de_* fields from perfSummary
(success-only).

- RenderCaptureObservability now also records dePreInversionWorkers /
  dePreRouterWorkers — the worker count calibration would have picked absent
  the experiment — so a resource-pressure failure can be correlated with the
  router overriding a lower calibrated count.
- New capture-sourced de_* fields on RenderObservabilityTelemetryPayload,
  shared by trackRenderComplete and trackRenderError. Explicit
  perfSummary-sourced fields still win on render_complete (spread moved
  first in the event object) — this is purely a failure-path fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 15:31:58 -07:00
Vance IngallsandClaude Fable 5 87e2a70f9a fix(core,cli): address PR review — consistent 403 error shape, cap Retry-After, URL-safe ref split
Rames's inline findings on #2112:
- forbiddenError now RETURNS in every branch (BAD_TOKEN no longer throws
  inside) so the caller's single throw covers all cases — no mixed
  throw/return contract for a future wrapping caller.
- retryAfterMs capped at 60s: a spec-legal Retry-After: 3600 no longer
  silently blocks the CLI for an hour before RATE_LIMITED.
- asset ref gathering extracted to gatherAssetRefs() and made URL-safe:
  bare fileKey:nodeId tokens comma-split, but a figma URL with commas in
  its query (multi-select node-id=1:2,3:4) is kept whole.
- Documented in SKILL that 429 retry lives in the shared request path, so
  EVERY read endpoint retries (not just asset) — blast-radius note the
  reviewer asked for. variables intentionally still retries: its fallback
  is REQUIRES_ENTERPRISE-only, and a 429 there is transient, not a gate.

Tests: retry-cap (3600→60000), non-styles endpoint retry, gatherAssetRefs
URL-vs-bare split. client 24, cli asset 11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:31:11 -07:00
Vance IngallsandClaude Fable 5 1d18b30625 test(cli): add renderNodes to tokens.test figma client mock
CI typecheck caught the tokens.test mock missing the new renderNodes
member on FigmaClient (asset/component mocks were updated, this one was
missed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:22:56 -07:00
Vance IngallsandClaude Fable 5 4fc699fee6 fix(core,cli): parse figma 403 body, batch asset fetch, fix NO_TOKEN box
Extends the scope+retry work from the figma bug-bash (valid report:
9-bugs-with-repros; the skill-not-used report was discarded).

- 403-body parse (bug 4): figma returns 403 {"err":"Invalid token"} for bad
  PATs (NOT 401), and 403 {"err":"Invalid scope(s)… requires X"} for missing
  scopes. get() now reads the body: "Invalid token" reclassifies to BAD_TOKEN
  with re-mint advice; a scope body surfaces figma's own diagnosis verbatim;
  else falls back to the endpoint's scope hint. Reads both err and message
  (variables endpoint uses message). One fix, honest messages for bugs 1/4/9.

- Batch asset fetch (requested): figma asset accepts multiple refs
  (space-separated or comma-joined) of one file and renders them in a SINGLE
  /v1/images call via new client.renderNodes — figma's documented per-minute
  rate-limit workaround. runAssetImport delegates to runAssetImportMany;
  cache-checks per node, batches only the misses, one index.md regen.

- NO_TOKEN box (bug 8): errorBox indented only the first hint line, mangling
  the numbered setup list. Indent every line; single-line hints unchanged.

Verified live: 3 refs -> 3 imports -> 1 request; bad token -> BAD_TOKEN not
scope advice. Client suite 22, cli figma 33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:14:57 -07:00
Vance IngallsandClaude Fable 5 1bb7688347 fix(core): name missing figma scope in 403, retry 429 with backoff
Two bugs from live figma-integration use:

1. `tokens` styles fallback 403s on non-Enterprise. /v1/files/:key/styles
   needs library_content:read — a scope the setup docs and the generic
   FORBIDDEN message both omitted, so the user saw "missing a read scope"
   with no way to know which. Each endpoint now carries a scope hint; the
   403 names the exact scope (styles → library_content:read). Setup text and
   skill scope list updated to include Library content: Read-only.

2. `asset` (and every per-node component render) had no 429 handling — the
   message said "back off and retry" but the client didn't. Two imports in
   a row tripped the per-minute limit and hard-failed. get() now retries 429
   with exponential backoff, honoring Retry-After when present, before
   surfacing RATE_LIMITED after maxRetries (default 3). sleep is injectable
   so tests don't wait.

Batch multi-node asset syntax (the documented /v1/images comma-ids rate
workaround) is a separate enhancement — retry makes the reported failure
self-heal, including the many-node component path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 13:31:13 -07:00
Vance Ingalls 030fded71d chore: release v0.7.46 2026-07-09 12:01:33 -07:00
Vance Ingalls 65dd2dc77e Merge pull request #2093 from heygen-com/fix/static-dedup-tlcall-disqualify
fix(engine): disqualify static-frame dedup on any tl.call()
2026-07-09 12:00:16 -07:00
Vance Ingalls 7acce7e4f4 Merge pull request #2108 from heygen-com/docs/sdk-attachsync-and-variable-crud
docs(sdk): document attachSync, variable CRUD, and getRootElements/getAllAnimationIds
2026-07-09 11:59:28 -07:00
Vance Ingalls 70b9f4dad0 Merge pull request #2100 from heygen-com/feat/sdk-live-dom-sync
feat(sdk): attachSync mirrors composition edits onto a live document
2026-07-09 11:58:39 -07:00
Vance Ingalls bf936f5e3e Merge pull request #2098 from heygen-com/feat/sdk-variable-crud-and-exports
feat(sdk): variable CRUD (declare/remove/get/list) + export gaps
2026-07-09 11:53:58 -07:00
Vance Ingalls f169d5fad6 docs(sdk): document CompositionVariable, id/variable utilities, and attachSync in canvas guide
Follow-up to the previous commit in this PR — found while auditing whether
any SDK-surface documentation gaps existed beyond this stack:

- types.mdx: EditOp's own union listing was missing declareVariable/
  removeVariable (present in edit-operations.mdx's table but not mirrored
  here). Adds a full CompositionVariable reference section (base fields +
  all 7 variants) since composition.mdx's new declareVariable/listVariables
  docs reference it without it being defined anywhere in the type reference.
- utilities.mdx: documents 6 exported functions with zero prior docs —
  resolveScoped, findById, bareId, escapeHfId, isNewHostBoundary (Id & Scope
  Utilities) and readVariableDefault (Variable Utilities). Pre-existing gaps,
  unrelated to this stack.
- canvas-integration.mdx: adds a "Keeping the preview in sync" section
  covering attachSync right where the guide already sets up preview + comp —
  previously the guide never mentioned it despite being exactly the answer
  to "how do I keep the iframe in sync with edits."
2026-07-09 11:52:14 -07:00
Vance Ingalls c94de6034e fix(sdk): address PR #2100 review feedback on attachSync
- Script-mirror filter changed from an exact "/script/gsap" match to
  path.startsWith("/script/") — the documented contract is "never mirror
  script-tag rewrites," not just today's one known path; startsWith covers
  any future script-kind patch under the same intent.
- _syncDetach is now cleared when the caller invokes the returned detach
  function directly, not only on the next attachSync call — avoids holding
  a stale (already-unsubscribed) reference between an explicit detach() and
  a later attachSync(other).
- The initial applyOverrideSet call is now wrapped in try/catch: a bad
  initial snapshot no longer prevents the ongoing patch subscription from
  attaching, matching the SDK's existing swallow-and-warn precedent for
  silent-failure paths (adapters/iframe.ts's tainted-canvas warning).
- Added a test proving declareVariable/removeVariable (the /variable-decls/
  patches PR #2098 introduces) mirror onto the live document's
  data-composition-variables attribute — the existing suite only covered
  setVariableValue's CSS-custom-property path, not the schema-metadata path.
2026-07-09 11:52:14 -07:00
Vance Ingalls 91cc67a007 docs(sdk): document attachSync, variable CRUD, and getRootElements/getAllAnimationIds
These SDK reference docs were behind the API surface: PR #2100's attachSync
had zero documentation, and PR #2098/#2092's declareVariable, removeVariable,
getVariableValue, listVariables, and getRootElements were all missing from
composition.mdx despite being real public Composition methods. getAllAnimationIds
was also undocumented (pre-existing gap, unrelated to this stack).

- composition.mdx: adds getVariableValue, listVariables, declareVariable,
  removeVariable (Typed edit methods), getRootElements, getAllAnimationIds
  (Query section)
- adapters.mdx: adds attachSync to the PreviewAdapter interface + a
  ParamField documenting its contract (immediate sync, ongoing patch
  mirroring, script-patch exclusion, detach semantics)
- edit-operations.mdx: adds declareVariable/removeVariable rows + examples
  to the Variables op table
2026-07-09 11:52:14 -07:00
Vance Ingalls fbb6a33c91 test(sdk): cover attachSync mirroring for setVariableValue and setTiming
setVariableValue is the headline case the sync spec was built for and
had no coverage; setTiming (data-start/data-end mirroring) was also
untested. Both regression-checked by temporarily breaking the
underlying mutate/apply-patches code paths and confirming the new
assertions fail.
2026-07-09 11:52:14 -07:00
Vance Ingalls 393eced906 fix(sdk-playground): implement PreviewAdapter.attachSync no-op stub
PlaygroundPreview implements PreviewAdapter but was missing attachSync,
which this branch added to the interface — a real TS break (no
typecheck script wires sdk-playground into CI, so nothing caught it).
Mirrors the same no-op stub already added to HeadlessPreviewAdapter.
2026-07-09 11:52:13 -07:00
Vance Ingalls 9c691d2438 test(sdk): cover attachSync re-attach teardown and manual detach 2026-07-09 11:52:13 -07:00
Vance Ingalls 0e08543561 test(sdk): cover attachSync against a detached iframe 2026-07-09 11:52:13 -07:00
Vance Ingalls ed9df3f57d test(sdk): cover script-patch skip and stylesheet-patch mirroring in attachSync 2026-07-09 11:52:13 -07:00
Vance Ingalls 91ddf9e196 feat(sdk): attachSync mirrors composition edits onto a live document
Adds attachSync(comp) to PreviewAdapter/IframePreviewAdapter — does an
immediate full sync via the existing applyOverrideSet, then subscribes to
comp.on('patch', ...) and replays every future patch (forward or inverse —
undo/redo included) via the existing applyPatchesToDocument, pointed at the
iframe's live document instead of the offscreen linkedom one. No new
mutation logic; both functions already work against any
{document, wrapped, stamped}-shaped object.

Also adds a no-op attachSync stub to HeadlessPreviewAdapter, required to
keep it satisfying the widened PreviewAdapter interface.

Closes the gap that made pacific's canvas-react hand-roll its own
override-application code (applyOverrideToIframe.ts) with two separate
mechanisms (diffing for normal edits, verbatim op-replay for undo/redo) —
subscribing to the patch stream directly needs only one.
2026-07-09 11:52:13 -07:00
Vance Ingalls bcda11aa7c fix(sdk): address PR #2098 review feedback on variable CRUD
- validateOp now handles declareVariable/removeVariable (E_NO_ROOT when no
  composition root), matching setVariableValue's existing case — previously
  comp.can() returned E_UNKNOWN_OP for both.
- removeVariable's undo-inverse now tags its {decl, index} reinsert payload
  with __kind: "reinsert" instead of relying on structural "decl"/"index"
  key presence to disambiguate it from a plain declareVariable patch.
  VariableDecl has an open index signature, so a real variable schema could
  legally declare its own "decl"/"index" fields and be misinterpreted by the
  old structural check; a regression test pins the exact collision.
- getVariableValue's return type tightened from `unknown` to
  `string | number | boolean | FontValue | ImageValue | undefined`, matching
  setVariableValue's parameter type for round-trip symmetry. The underlying
  unknown-typed read is cast once at this SDK boundary.
- Added a redo test for declareVariable/removeVariable (existing tests only
  covered undo).
2026-07-09 11:52:12 -07:00