The prepareFlattenedInnerRoot function creates a wrapper div when
inlining sub-compositions. This wrapper had no width/height, which
broke CSS height:100% chains — any sub-composition using percentage
heights with flexbox centering would collapse to 0px and render
content at the top instead of centered.
Read data-width/data-height from the inner root and set matching
pixel dimensions on the wrapper's inline style. Applied in both the
compiler (server-side bundling) and the runtime (browser-side
composition loader).
Adds a producer regression test with a centered card sub-composition
that fails without this fix.
import.meta.env is undefined in Next.js Turbopack/Webpack, causing
"Cannot read properties of undefined" when the studio telemetry client
loads. Wrap accesses in try-catch so they gracefully fall back.
Also hardcode the PostHog API key and host — they're public write-only
values with no reason to be overridable via env.
When the distributed render path stitches chunks with `-c copy`,
ffmpeg averages the container framerate from PTS rather than
carrying the source's exact rational rate, producing values like
`360000/12001` instead of `30/1` and ~5ms duration drift over
60s.
This is a known ffmpeg behavior at the concat-demuxer-copy
boundary. The industry-standard fix is `-r <fps>` as an input
flag on the concat step plus an output flag on the subsequent
mux step — both with `-c copy` retained, no re-encode required.
Three sites updated:
- `assemble.ts` concat step: `-r <fps>` input flag.
- `chunkEncoder.muxVideoWithAudio`: `-r <fps>` output flag.
- `chunkEncoder.applyFaststart`: same, threaded from caller.
Adds `r_frame_rate` + duration-equivalence assertions to
`assemble.test.ts` to close the regression hole.
Remaining review follow-ups:
- killProcessTree now escalates to SIGKILL after 500ms if SIGTERM
doesn't kill the process (same pattern as killTrackedProcesses).
Covers orphan cleanup and dev/local mode tree kill.
- Added unit tests for both new modules:
- processTracker.test.ts (6 tests): track/remove on exit/error,
kill running processes, SIGKILL escalation for SIGTERM-resistant
processes, idempotency.
- orphanCleanup.test.ts (5 tests): tree kill with children,
SIGKILL escalation, non-existent PID handling, orphan detection
returns 0 when clean.
- Blocker: arm 3s force-exit timer BEFORE awaiting cleanup, not
inside .finally(). Prevents hang if drainBrowserPool() blocks on
dead Chrome.
- Reorder cleanup: killTrackedProcesses() (sync, fast) runs first,
then async browser drain. Ffmpeg dies immediately instead of
surviving if the hard timer fires early.
- SIGKILL escalation: processTracker now SIGTERMs all tracked
processes, then SIGKILLs survivors after 500ms grace period.
- Scope pgrep to current user (pgrep -u $(id -u)) so orphan
detection doesn't touch other users' Chrome on shared machines.
- Add process.on('exit') handler for crash paths (unhandled
exceptions/rejections that bypass signal handlers).
- Document Windows no-op behavior on killProcessTree handlers.
FFmpeg's VFR-to-CFR normalization produces slightly different frame
counts across versions due to timestamp rounding in the fps filter.
The ±1 tolerance was too tight for Linux FFmpeg builds. Widen to ±3
frames — still catches the 25% shortfall regression these tests
guard against.
The preview command's shutdown handler only closed the HTTP server,
leaving Chrome (browser pool) and ffmpeg processes alive. This caused
silent resource leaks — orphaned processes consuming CPU and RAM with
no parent.
Root cause: preview.ts never called drainBrowserPool() or killed
tracked ffmpeg processes. The thumbnail browser in studioServer.ts
registered its own competing signal handlers that raced with
preview's shutdown.
Fix:
- Add a central process tracker (processTracker.ts) that registers
every spawned ffmpeg across engine and producer packages
- Centralize thumbnail browser cleanup via exported
closeThumbnailBrowser() instead of scattered signal handlers
- Wire preview shutdown to call closeThumbnailBrowser(),
drainBrowserPool(), and killTrackedProcesses() before closing the
HTTP server (embedded mode)
- Add killProcessTree() for dev/local modes where Chrome runs in a
child process tree
- Add startup orphan detection that finds and kills orphaned
chrome-headless-shell/Puppeteer Chrome processes (PPID=1) from
previously crashed sessions
Closes#1038
Split PlayerControls.tsx into focused sub-components (SeekBar,
WorkAreaOverlay, MuteButton, LoopButton, FullscreenButton,
ShortcutsPanel, SpeedMenu) and extracted seek bar drag/progress
tracking into useSeekBarDrag hook.
Split manualEditsDom.ts patch-builder functions into
manualEditsDomPatches.ts with data-driven helpers to reduce
duplication and complexity.
Extracted per-type reapply helpers from reapplyPositionEditsAfterSeek
and factored out identity-matrix check from
stripGsapTranslateFromTransform.
Raised file-size limit from 500 to 600 lines, removed
.filesize-allowlist.
Audit of every detection rule in the registry against actual vendor
source code. Rules that lacked a public-source citation were guesses
and have been removed; surviving rules now all cite the file + line
that emits the marker.
Codex — replace per @magi's investigation:
- Drop CODEX_HOME (config override read at startup, NOT propagated to
child processes — would miss most Codex invocations).
- Drop CODEX_SANDBOX (macOS Seatbelt only; covered by the others).
- Add CODEX_THREAD_ID (set unconditionally on every spawned shell
command — codex-rs/protocol/src/shell_environment.rs:6 +
codex-rs/core/src/unified_exec/process_manager.rs:1010).
- Add CODEX_CI (hardcoded in UNIFIED_EXEC_ENV — process_manager.rs:70).
- Keep CODEX_SANDBOX_NETWORK_DISABLED (default-on sandbox marker —
codex-rs/core/src/sandboxing/mod.rs:135-138).
Cursor — drop unverified CURSOR_TRACE_ID and CURSOR_AGENT guesses.
Keep TERM_PROGRAM=cursor (set by Cursor's integrated terminal).
Pi — new rule. https://github.com/earendil-works/pi
packages/coding-agent/src/cli.ts:13 unconditionally executes
process.env.PI_CODING_AGENT = "true";
at module entry, so every subprocess Pi spawns sees this marker.
Same propagation pattern as Hermes.
Removed (no source-cited marker found in this audit):
- aider — verified Aider sets no AIDER_* env vars; only OR_SITE_URL and
OR_APP_NAME (OpenRouter integration). No reliable marker.
- gemini_cli — GEMINI_SANDBOX/GEMINI_CLI_TRUST_WORKSPACE are conditional
on CLI flags; no unconditional marker found.
- jules, devin — closed source, no public marker documentation.
These vendors can be re-added later with a source citation; absence
in the registry will silently false-negative (events land in the null
bucket), but won't false-positive on other vendors.
Per @james-russo's review: do source-level research before shipping
detection rules. Memory updated to enforce this for future work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from @miguel-heygen's review:
1. HERMES_QUIET — switch to existence check.
`env["HERMES_QUIET"] === "1"` was brittle vs. future Hermes changes
(e.g. if cli.py ever sets it to "true"). The var name itself is
specific enough that existence is the right signal.
2. CI_PROVIDERS — convert to a discriminated union.
`mode: "truthy" | "presence"` is stricter than the previous pair of
optional boolean flags (which allowed entries with neither set).
3. Sandbox detection tests — add coverage.
- Docker positive: /.dockerenv present → docker.
- Negative case: plain Linux laptop with no markers → null.
Together with the gVisor 4.4.0 fix in the previous commit, that addresses
all three actionable callouts (the discriminated-union nit was non-blocking
but worth doing while in the file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR feedback from @magi: kernel string `4.4.0` is also the
Ubuntu 16.04 LTS / older-real-kernel version, so accepting it alone
false-positives. Now `4.4.0` only counts as gVisor when /proc/version
also contains "gVisor". `*-gvisor` kernel strings remain standalone-
sufficient since no real production kernel reports them.
Adds a regression test that an Ubuntu 16.04 box reporting
`Linux version 4.4.0-1128-aws (buildd@lcy01)` is NOT classified as
a gVisor sandbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two new properties to every CLI telemetry event so we can tell
managed-sandbox traffic (Codex Cloud, Claude Code Web, etc.) apart from
real developer laptops without geolocation guesswork:
- sandbox_runtime: 'gvisor' | 'firecracker' | 'docker' | 'kvm' | 'wsl' | null
gVisor detected via kernel string ('4.19.0-gvisor' or legacy Sentry
'4.4.0') + /proc/version. Firecracker via /dev/vsock + DMI sys_vendor.
Docker reuses the existing /.dockerenv + cgroup probe.
- agent_runtime: claude_code | codex | cursor | copilot_agent | jules
| replit | devin | aider | gemini_cli | hermes | openclaw | null
Detected by the EXISTENCE of well-known vendor env vars only — values
are never read. Hermes rule keys on HERMES_QUIET=1 (set unconditionally
at hermes-agent/cli.py:50). openclaw rule keys on OPENCLAW_STATE_DIR
or OPENCLAW_CONFIG_PATH (set explicitly in the spawned child env at
openclaw/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts).
Drive-by cleanups required by fallow because system.ts and client.ts
fall into the audit scope of this PR:
- Extract detectWSL into platform.ts to break the system.ts ↔ agent_runtime.ts cycle.
- Refactor detectCI / getCIName into a single CI_PROVIDERS table.
- Dedupe flush / flushSync via a shared drainQueueToPayload helper.
Privacy posture unchanged: HYPERFRAMES_NO_TELEMETRY=1 still opts out;
disclosure in docs/packages/cli.mdx updated to enumerate the new fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repeated Sandbox.Timedout chunks can leave @sparticuz/chromium
returning a falsy/empty path on subsequent invocations — warm
instances on the same execution environment never re-extract
chromium. The downstream puppeteer-core assertion about needing an
executablePath or channel buries the actionable cause; a cost-
analysis sweep took ~30 min to root-cause from that trace.
Guard the resolver: if mod.executablePath() returns a non-string,
empty string, or a path that does not exist on disk, throw a typed
ChromeBinaryUnavailableError whose message points at the recycle
remedy (env-var bump or redeploy). Add the error name to the three
NON_RETRYABLE lists so SFN short-circuits instead of burning four
15-min retries on a function that won't recover.
Same typed-error contract for the chrome-headless-shell fallback so
both sources fail consistently. Tests pin the wedge path (empty
string + non-existent file) and the carried metadata (source +
resolvedPath).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`--width 3840 --height 2160` against a composition with
`data-width="1920"` silently produces a 1080p output because the
runtime lays out the page at the composition's authored dimensions —
real footgun we hit during a cost-analysis sweep. Warn early and point
at `--output-resolution` (the supersampling escape hatch) so the user
doesn't burn a 30-minute render learning the override rule.
Skipped when `--output-resolution` is set (the supported supersampling
path — the user is opting in), when `--json` is set (machine consumers),
or when `index.html` isn't on disk (typical with `--site-id`).
Helper lives in a shared module so render + render-batch agree on the
parse + message. Tests cover both attribute orders, single/double
quotes, the silent paths, and the warning path. Best-effort regex over
the canonical attr shape — malformed HTML falls through to no warning
rather than blocking the render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CDK construct compiles tasks.LambdaInvoke to the optimized
arn:aws:states:::lambda:invoke integration, which emits Task* history
events with the Lambda response wrapped in .Payload. getRenderProgress
was only listening for the older LambdaFunction* events, so every CDK-
deployed stack reported $0 total cost and zero invocations on success
— a high-visibility regression that only surfaced when we manually
walked SFN history during a cost-analysis sweep.
Add cases for TaskScheduled (count invocation), TaskSucceeded (parse
Payload + accumulate billed duration / frame counts), and TaskFailed
(record error). Keep the LambdaFunction* paths so anyone wiring the
raw lambda:invokeFunction.sync task type still works. Factor out the
shared FramesEncoded-attribution logic so both branches agree on the
"only RenderChunk frames count" rule.
Tests pin a real-shape regression: replay the inspector-launch
1080p/30fps history (1 Plan + 16 RenderChunks + 1 Assemble) and assert
lambdaUsd lands at ~$0.582 — matching the cost-analysis script's
direct read against SFN history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Allows authored-at-1080p compositions to render at 4K/2K via Chrome
deviceScaleFactor supersampling without re-laying-out the composition.
Plain --width 3840 silently lays out at 1920×1080 because data-width/
data-height attrs override Config.width — this flag is the supported
way to ask the renderer to supersample.
Accepts canonical CanvasResolution names (landscape, landscape-4k,
portrait, portrait-4k, square, square-4k) and aliases (1080p, 4k, uhd,
hd, 1080p-portrait, 4k-portrait, 1080p-square, 4k-square). Wired
through render + render-batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rational `Fps = { num, den }` refactor in 5dcc89c broke callers
passing `fps: 30` (the form documented in every code example and used
by external consumers). FFmpeg received `undefined/undefined` as the
framerate, causing a cryptic exit-code error.
Add `FpsInput = number | Fps` and `toFps()` normalizer in
@hyperframes/core. `createRenderJob` now accepts both forms —
plain integers are promoted to `{ num, den: 1 }` at the boundary;
`RenderConfig.fps` stays strict `Fps` internally so no downstream
code changes.
Also fixes the producer and engine docs, which showed phantom
`input`/`output` fields on `createRenderJob` and a wrong
`executeRenderJob(job)` signature (missing `projectDir`/`outputPath`
args).
Closes#1031
Two fixes for the 3M+ unhandled_promise_rejection events/day spike:
1. Filter: suppress "Error fetching ... 404" rejections from composition
code — these are asset-not-found content errors, not Studio bugs.
2. Rate-limit: cap both error and rejection telemetry at 50 per session.
After the cap, emit a single *_cap_reached event so we know capping
occurred without generating unlimited events.
3. Root cause: webAudioTransport now checks response.ok before decode
and caches failed URLs in _failedSrcs so repeat ticks don't re-fetch
the same 404 on every playback frame.
Also add playground/ to fallow ignorePatterns — local experiment
directory was tripping the audit gate.
Flip the fallback from false to true so the blocks panel is on for
everyone out of the box. Users can still disable it via
VITE_STUDIO_ENABLE_BLOCKS_PANEL=false if needed.
`injectInterceptor` used `String.prototype.replace(target, replacement)`
to inject the runtime `<script>` before `</head>`. The replacement
string is a substitution template — `$&` expands to the matched
substring, and the minified runtime IIFE contains legitimate `$&`
sequences (e.g. `if(te&&$&!y.hasAttribute(...))`), so every `$&` in
the body was silently rewritten to `</head>`, producing
`Unexpected token '<'` SyntaxErrors and breaking every timeline in
the bundle.
Switch to the function-replacer form so the runtime body is passed
through verbatim. Add a regression test that diffs the bundled
runtime body against `getHyperframeRuntimeScript()` and asserts only
one `</head>` survives in the document — the test exercises the
`<head>`-present injection path (the only branch that uses the
substitution template; the no-`<head>` fallback uses slice+concat
and was unaffected).
Only the bundler is affected — `producer/fileServer.ts` already uses
the function form via `injectScriptsIntoHtml` in
`htmlDocument.ts`, so render output was correct. Snapshot, preview,
studio, layout, and validate all consume `bundleToSingleHtml` and
were broken before this fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Create a Tooltip component with styled popover (dark bg, border,
shadow) that appears on hover with a 400ms delay. Applied to:
- Left sidebar tabs: Code, Comps, Assets, Catalog
- Right panel tabs: Design, Layers, Motion, Renders
Replaces native title attributes with proper styled tooltips.
Add title attributes to interactive elements that were missing them:
- PlayerControls: Play/Pause, playback speed, shortcuts panel, clear
in/out-point buttons, and jump-to-frame Go button
- StudioRightPanel: Design, Layers, Motion, and Renders tab buttons
Call tl.pause() both before AND after tl.seek() in the adapter.
GSAP's seek() can reactivate a timeline depending on internal state;
the second pause() guarantees it stays frozen at the seeked position.
When seeking via the slider or timeline scrub, explicitly pause all
<video> and <audio> elements inside the preview iframe. The GSAP
timeline pauses but iframe media elements can continue playing
independently, causing audio to keep going after a seek.
Clicking "Ask agent" now opens a modal that shows the full generated
prompt so the user can read it before copying. The modal has a "Copy
prompt" button that turns green on success. This replaces the silent
clipboard copy that gave no visibility into what was copied.
- Ask agent button turns green with checkmark on copy
- Add button shows tooltip "Add to composition at current time"
- Ask agent shows tooltip "Copy a prompt to paste into your AI agent"
- Tab tooltips: Code, Comps, Assets, Catalog each explain their purpose
- Search placeholder updated to "Search by name, category, or tag…"
Add button rules: VFX, Social, Scenes get Add + Ask agent. Captions,
Transitions, Effects, Data get Ask agent only.
Search now matches category names and tags, so searching "captions"
shows all caption blocks.
Components (hyperframes:component) get both "Add" and "Ask agent"
buttons since they work as drop-in overlays. Blocks (scenes, data,
VFX, transitions) show only "Ask agent" since they need agent-guided
customization.
The "Ask agent" button now copies the current composition state along
with the category-specific prompt: playback time, active composition
path, dimensions, and all elements visible at the current time with
their track, timing, and source paths. Gives the agent full context
to place and customize the block correctly.
When previewing a component (compositions/components/*), render the
main composition player as a backdrop behind the transparent component
overlay. This lets users see captions, vignettes, and other overlays
in context instead of against a black void.
Registry blocks are authored at 1920x1080 but projects may use
different dimensions (e.g. 1280x720). After installing a block, the
server now reads the host project's data-width/data-height from
index.html and rewrites the block's viewport meta and CSS dimensions
to match, preventing overflow.
Remove the Add button and drag-and-drop from catalog cards — blocks
and components need agent-guided customization, not blind insertion.
Each card now shows a single "Ask agent" button that copies a rich,
category-specific prompt to clipboard with context about what the
block does and how to customize it (captions: transcribe + style,
transitions: place at cut point, data: replace values, etc.).
Each catalog card now shows two hover buttons:
- "Add" — inserts the block/component into the composition at the
current playhead position (existing behavior, now with + icon)
- "Agent prompt" — copies a contextual prompt to clipboard tailored
to the block's category (captions, transitions, VFX, etc.) so the
user can paste it into their AI agent for guided customization
Blocks and components now start at the current playhead position
instead of being appended after all existing content. If the new
element extends beyond the root composition's data-duration, the
root is automatically extended to fit.
insertTimelineAssetIntoSource now detects the parent indent level and
adds the new element with matching child indentation. Block attributes
are written one-per-line for readability.