Skip to content

fix(engine): auto-fall back to screenshot mode when chrome-headless-shell drops HeadlessExperimental.beginFrame - #296

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/auto-fallback-screenshot-mode-chromium-147
Apr 16, 2026
Merged

fix(engine): auto-fall back to screenshot mode when chrome-headless-shell drops HeadlessExperimental.beginFrame#296
miguel-heygen merged 3 commits into
mainfrom
fix/auto-fallback-screenshot-mode-chromium-147

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #294.

Summary

Recent chrome-headless-shell builds (observed on 147) no longer expose HeadlessExperimental.beginFrame. The domain's enable/disable methods are deprecated upstream and appear to have been dropped alongside beginFrame in these builds, so on Linux with chrome-headless-shell the engine aborts with

```
Protocol error (HeadlessExperimental.beginFrame):
'HeadlessExperimental.beginFrame' wasn't found
```

and — because the browser was launched with --enable-begin-frame-control — the compositor waits for beginFrames the engine can no longer deliver, so every subsequent screenshot also comes back blank. Today users have to discover PRODUCER_FORCE_SCREENSHOT=true themselves (openclaw did exactly that — see the issue body).

Fix

One-time probe, right after the browser launches in beginframe mode:

  1. Create a disposable CDP session.
  2. await client.send("HeadlessExperimental.enable").
  3. Send one no-op HeadlessExperimental.beginFrame raced against a 2s timeout.
  4. If anything throws / times out — missing method, protocol error, stuck call — close the browser, strip beginframe-only chrome flags, relaunch in screenshot mode, and set `captureMode = "screenshot"` for the returned session.

Probing beginFrame directly rather than enable alone is important because some builds keep the domain registered (so .enable() succeeds) while dropping the method itself — that's exactly the failure shape in #294.

Cost on happy path: one extra CDP round-trip per browser acquisition (≈ a few ms, since in beginframe-control mode the command returns as soon as the compositor acks). Cost on broken path: one extra launch, which is what the env-var escape hatch already forces manually.

The beginframe-only flag set is enumerated in-module and matched by the stripper, so adding/removing flags stays in one place with buildChromeArgs.

Test plan

  • bun run --filter=@hyperframes/engine test — all 42 tests pass
  • bun run --filter=@hyperframes/engine build — typechecks
  • bunx oxlint + bunx oxfmt --check clean
  • Manual: standalone test on Linux x86_64 with chrome-headless-shell 146 — probe returns supported=true, no fallback (happy path)
  • Manual: same test with --force-fail simulating openclaw's missing-method condition — fallback triggers, flags stripped, relaunch succeeds, 6.8 KB PNG captured (broken path)
  • Verify on openclaw / real chrome-headless-shell 147 build that the fallback triggers automatically without PRODUCER_FORCE_SCREENSHOT

Notes

  • probeBeginFrameSupport catches any failure generically; we trust that a working browser answers the no-op beginFrame in well under 2s.
  • Warning is logged once per browser acquisition, not per frame.
  • Browser pool interaction: pooled browsers cache the resolved captureMode, so subsequent acquires in the same process reuse the post-fallback mode without re-probing.

Chromium 132+ removed the HeadlessExperimental domain. On Linux with
chrome-headless-shell we default to beginframe capture, which silently
produces blank frames on newer Chromium because `--enable-begin-frame-control`
leaves the compositor waiting for beginFrames the code can no longer send.
Users had to discover PRODUCER_FORCE_SCREENSHOT=true themselves.

Probe HeadlessExperimental.enable() on a disposable page right after launch.
If the domain isn't there, close the browser, relaunch with the beginframe-
only chrome flags stripped, and mark the session as screenshot mode. Users
on supported Chromium versions are unaffected — the probe adds one CDP
round-trip to browser acquisition and nothing else.

Closes #294.
@vanceingalls

Copy link
Copy Markdown
Collaborator

Fact-check on the description's version attribution — the code looks right, but the "Chromium 132 removed the entire HeadlessExperimental CDP domain" line isn't accurate.

What Chromium 132 actually did: removed the old --headless mode from the main Chrome binary. chrome-headless-shell was introduced precisely to preserve that old-headless behavior as a standalone binary (Chrome for Developers blog).

What's actually in tip-of-tree Chromium today (HeadlessExperimental.pdl on main):

  • beginFrame — still present, not deprecated
  • enable / disable — deprecated (but still listed)

Issue #294 itself pins the failure on Chromium 147, which matches the branch name. Whatever dropped beginFrame in the user's environment happened later than 132 and appears specific to recent chrome-headless-shell builds, not the CDP domain at large.

Practical risk for the fix: the probe calls HeadlessExperimental.enable, which is deprecated upstream. If a future chrome-headless-shell drops .enable() while keeping .beginFrame() (plausible — that's the direction the PDL is pointing), the probe produces a false negative and silently downgrades to screenshot mode on a browser that still supports beginframe. Probing .beginFrame directly with a throwaway no-op frame would be a truer test. If you've empirically confirmed both disappear together on 147, .enable is a reasonable proxy, but worth noting.

Suggested tweak — rewrite the first paragraph roughly as:

Recent chrome-headless-shell builds (observed on 147) no longer expose HeadlessExperimental.beginFrame. The domain's enable/disable methods are deprecated upstream and appear to have been dropped alongside beginFrame in these builds, so the engine aborts with 'HeadlessExperimental.beginFrame' wasn't found and every screenshot comes back blank.

Code itself LGTM — just the version attribution.

First version of the probe only called `HeadlessExperimental.enable`, but
some Chromium builds keep the domain registered (so enable() succeeds) and
drop the beginFrame method itself. The warmup loop then crashes on first
frame with `'HeadlessExperimental.beginFrame' wasn't found` — exactly the
failure mode reported in #294.

Race one cheap beginFrame call against a 2s timeout. Any failure — method
missing, timeout, generic protocol error — is treated as unsupported and
the caller relaunches in screenshot mode. Verified against chrome-headless-
shell 146 on Linux x86_64 (real beginFrame support, probe returns true) and
forced-fail mode (probe returns false → strip beginframe flags → relaunch →
valid 6.8 KB PNG captured).

Found via devbox testing #294 repro on Linux.
Chromium 132 removed the old `--headless` mode from the main Chrome
binary, not the HeadlessExperimental CDP domain. The domain is still
upstream (enable/disable are deprecated but beginFrame is not) — what
actually dropped in user reports is chrome-headless-shell 147+ builds,
which is where issue #294 reproduces.

Rewrites the doc comment and the inline comment in acquireBrowser to
attribute the breakage to chrome-headless-shell 147 rather than all of
Chromium 132+. No behavioural change.

Per review feedback on #296.
@miguel-heygen miguel-heygen changed the title fix(engine): auto-fall back to screenshot mode on Chromium >= 132 fix(engine): auto-fall back to screenshot mode when chrome-headless-shell drops HeadlessExperimental.beginFrame Apr 16, 2026
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Thanks @vanceingalls — both points addressed:

1. Version attribution. Fixed in 7c0d1ae. Updated the PR title + description and the inline doc comments to pin the breakage on chrome-headless-shell 147+ rather than "Chromium 132+". You're right that 132 was the old --headless mode removal; the HeadlessExperimental domain itself is still upstream, and it's only recent chrome-headless-shell builds that dropped beginFrame.

2. Probe beginFrame directly, not just .enable. Done in 4dca292 (pushed before your review landed, same concern). The probe now:

  • calls HeadlessExperimental.enable first,
  • then races one no-op HeadlessExperimental.beginFrame against a 2s timeout,
  • returns false on any failure (missing method, timeout, protocol error).

This catches exactly the build shape you're worried about — .enable() present but beginFrame gone. Verified on a Linux x86_64 devbox with chrome-headless-shell 146 (probe returns true, happy path) and with a --force-fail flag simulating openclaw's condition (probe returns false → strip flags → relaunch → valid PNG captured).

Let me know if the current probe's failure handling is too broad — I generalised to catch {} rather than pattern-matching "wasn't found" strings since the timeout path needs to count as unsupported too. If you'd prefer a narrower catch with the timeout as a separate branch, happy to tighten.

@miguel-heygen
miguel-heygen merged commit 5e52e27 into main Apr 16, 2026
20 checks passed

Copy link
Copy Markdown
Collaborator Author

Merge activity

Zollicoff pushed a commit to Zollicoff/hyperframes that referenced this pull request Jul 1, 2026
Chromium 132 removed the old `--headless` mode from the main Chrome
binary, not the HeadlessExperimental CDP domain. The domain is still
upstream (enable/disable are deprecated but beginFrame is not) — what
actually dropped in user reports is chrome-headless-shell 147+ builds,
which is where issue heygen-com#294 reproduces.

Rewrites the doc comment and the inline comment in acquireBrowser to
attribute the breakage to chrome-headless-shell 147 rather than all of
Chromium 132+. No behavioural change.

Per review feedback on heygen-com#296.
dahans-msft2 pushed a commit to dahans-msft2/hyperframes that referenced this pull request Aug 6, 2026
…hell drops HeadlessExperimental.beginFrame (heygen-com#296)

Closes heygen-com#294.

## Summary

Recent `chrome-headless-shell` builds (observed on 147) no longer expose `HeadlessExperimental.beginFrame`. The domain's `enable`/`disable` methods are deprecated upstream and appear to have been dropped alongside `beginFrame` in these builds, so on Linux with chrome-headless-shell the engine aborts with

\`\`\`
Protocol error (HeadlessExperimental.beginFrame):
'HeadlessExperimental.beginFrame' wasn't found
\`\`\`

and — because the browser was launched with `--enable-begin-frame-control` — the compositor waits for beginFrames the engine can no longer deliver, so every subsequent screenshot also comes back blank. Today users have to discover `PRODUCER_FORCE_SCREENSHOT=true` themselves (openclaw did exactly that — see the issue body).

## Fix

One-time probe, right after the browser launches in beginframe mode:

1. Create a disposable CDP session.
2. `await client.send("HeadlessExperimental.enable")`.
3. Send one no-op `HeadlessExperimental.beginFrame` raced against a 2s timeout.
4. If anything throws / times out — missing method, protocol error, stuck call — close the browser, strip beginframe-only chrome flags, relaunch in screenshot mode, and set \`captureMode = "screenshot"\` for the returned session.

Probing `beginFrame` directly rather than `enable` alone is important because some builds keep the domain registered (so `.enable()` succeeds) while dropping the method itself — that's exactly the failure shape in heygen-com#294.

Cost on happy path: one extra CDP round-trip per browser acquisition (≈ a few ms, since in beginframe-control mode the command returns as soon as the compositor acks). Cost on broken path: one extra launch, which is what the env-var escape hatch already forces manually.

The beginframe-only flag set is enumerated in-module and matched by the stripper, so adding/removing flags stays in one place with `buildChromeArgs`.

## Test plan

- [x] `bun run --filter=@hyperframes/engine test` — all 42 tests pass
- [x] `bun run --filter=@hyperframes/engine build` — typechecks
- [x] `bunx oxlint` + `bunx oxfmt --check` clean
- [x] Manual: standalone test on Linux x86_64 with chrome-headless-shell 146 — probe returns `supported=true`, no fallback (happy path)
- [x] Manual: same test with `--force-fail` simulating openclaw's missing-method condition — fallback triggers, flags stripped, relaunch succeeds, 6.8 KB PNG captured (broken path)
- [ ] Verify on openclaw / real chrome-headless-shell 147 build that the fallback triggers automatically without `PRODUCER_FORCE_SCREENSHOT`

## Notes

- `probeBeginFrameSupport` catches any failure generically; we trust that a working browser answers the no-op beginFrame in well under 2s.
- Warning is logged once per browser acquisition, not per frame.
- Browser pool interaction: pooled browsers cache the resolved `captureMode`, so subsequent acquires in the same process reuse the post-fallback mode without re-probing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

issues installing on openclaw due to HeadlessExperimental.beginFrame

2 participants