fix(input): restore a dropped background lease before hidden input - #374
Merged
Merged
Conversation
When Chrome dropped a leased tab's focus override outside BackgroundExecution's applied-state cache, every input tool on that tab failed with input_not_ready until the session ended (Tencent#355). - BackgroundExecution.reapply() resends an owned override even when the cache reports it applied. It never forces a disable and keeps the applied record, so a failed resend is still disabled on release. - ChromiumCdp.restoreBackgroundExecution() checks ownership, then reapplies. - withInputReady restores through the lease owner, re-samples visibility and waits for two animation frames before dispatching. It does not toggle focus emulation or activate a window itself, and the override stays with the lease. If the page is still not visible, it returns input_not_ready before dispatch. - The frame wait uses requestAnimationFrame rather than a surface read: after a re-show, Chrome can leave Page.captureScreenshot({ fromSurface }) pending while frames already run. Closes Tencent#355
ArcueidMP
marked this pull request as ready for review
September 29, 2026 16:58
Contributor
Author
|
CI note: the only failing check, |
Merge the 0.3.2 release and keep the background-input fix under Unreleased. Isolate idle-exit tests from release checks and automatic daemon replacement. Wait for the first remote connection to complete its native handshake before testing replacement at capacity; the replacement connection still exercises the pre-handshake case.
iuyo5678
approved these changes
Sep 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #355. Follow-up to #311 and #242.
Problem
BackgroundExecutioncaches the focus override it applied per debugger attachment. When Chrome loses that override outside the cache (e.g.Emulation.setFocusEmulationEnabledis turned off from the extension service worker),synchronize()keeps skipping it because the cache still matches. Since #311, every input tool on that leased tab then fails closed withinput_not_ready— safely, but until the session ends.Change
BackgroundExecution.reapply(tabId)resends an owned override even when the applied cache reports it active. It never forces a disable, and it keeps the applied record while resending, so if the resend fails a later release still sends the disable (a returned page is never left emulated).ChromiumCdp.restoreBackgroundExecution(sessionId, tabId)checks that the session owns the lease, then reapplies. Exposed as an optionalCdpRunnermethod.withInputReady: when an owned lease sampleshidden, it restores through the lease owner, re-samples visibility, then waits for two animation frames before dispatching. It does not toggle focus emulation itself, does not activate any window, and leaves the override enabled for the next tool. If the page is still notvisible(or restore fails), it returnscdp_failed/input_not_ready/effect_state: nonewith no input sent. Runners withoutrestoreBackgroundExecutionkeep the fix(input): harden readiness around persistent background leases #311 behaviour.waitForInputPaintalready used (now shared aswaitForRendererFrames).Why animation frames instead of a surface read
The first version flushed with
Page.captureScreenshot({ fromSurface: true }), like the unowned fallback. The real-browser test caught it timing out intermittently. A probe on Windows 11 (3 parallel headless Chrome 153 instances at DPR 1.5, override dropped and restored each cycle):A second probe (180 cycles) delivered every click both with and without the double-rAF wait, and
visibilityStatewasvisibleright after the restore acknowledgement in every cycle; the rAF wait took up to ~1.9 s under that load. Visibility is checked before waiting, since frames stay suspended while hidden.This PR does not change the unowned fallback (a page hidden since creation), which still uses the surface read.
Validation
New tests, each confirmed to fail without the source change (all 8 new unit tests fail against
main's sources):chromium-cdp.test.ts: reapply resends despite the applied cache, rejects non-owners and released leases; a failed resend is still disabled on release while a passive reader keeps the attachment.interaction.test.ts: restore → visibility → frames → pointer ordering with no focus command fromwithInputReady, no surface read and noPage.bringToFront; restore that stays hidden, throws, or produces no frame reportsinput_not_readybefore dispatch; cancellation during restore.dispatcher.test.ts: realChromiumCdpwith a fake debugger modelling Chrome's override. The override is dropped outside the cache; the hidden-page click recovers through the lease owner and reaches the page (mouse events are rejected while hidden), the override stays enabled, and the next click needs no restore.click.browser.test.ts(real Chrome): drop the override on the same debugger session, then the leased click succeeds, the page records the trusted click, it isvisibleafterwards, the lease remains owned, and no surface read is used.Real-browser suites (click, background-execution, background-screenshot, background-full-page, observation-layers) run in parallel: 8/8 passes after switching to animation frames (the surface-read version failed 1 of 4 runs under the same load).
Full-stack fault injection on Windows 11, Microsoft Edge 154 at DPR 1.5, released
bsk0.3.1 CLI/daemon with an isolatedBSK_HOME, unpacked extension:bsk session start --no-focus, click once, minimize the Agent Window, disable focus emulation from the extension service worker (page reportshidden), thenbsk click:maininput_not_ready, exit 3input_not_readyThe temporary probe was not committed.
biome check, extensiontsc --noEmit, and the affected unit suites pass. On Windows,pnpm ext:testhit one timeout inhuman-loop.test.ts(child-process regex check) under full-suite load; it passes in isolation both with and without this change. The two kill-grace tests inpackages/dsh-plugin-browserskill/tests/runner.test.tsalso fail on unmodifiedmainon Windows and are unrelated.