This PR brings the new VOM feature and greatly improves BrowserSkill. Some strategies may need user feedback to catch edge cases, so let's merge it now as a new version and continue polishing it iteratively.
`enableConsoleDomains` marked the tab in `consoleDomainsAttemptedTabs`
before issuing `Runtime.enable`/`Log.enable` and swallowed any errors,
so a transient failure (e.g. a restricted page during attach)
permanently disabled console capture for that tab: every later
`ensureConsoleCapture` was a no-op and the agent silently got an empty
console buffer with no indication that capture was broken.
This was inconsistent with `enableNetworkDomain`, which records the tab
only after `Network.enable` succeeds (and is explicitly tested as
"retries and surfaces failures"). Bring `enableConsoleDomains` in line:
mark the tab only after both domains enable successfully, so a later
`ensureConsoleCapture` can recover from a transient failure. Renamed
the flag `consoleDomainsAttemptedTabs` -> `consoleDomainsEnabledTabs`
to match `networkDomainsEnabledTabs` and reflect the new semantics.
Attach remains best-effort (it still succeeds when a domain can't be
enabled). Adds a regression test asserting `ensureConsoleCapture`
retries after a failed enable; it fails (no retry) on the previous code.
Co-Authored-By: Claude <noreply@anthropic.com>
handleNavigate's `Page.frameNavigated` handler called
`acceptLifecycleEvent("commit", frameId)` without the frame's loaderId.
Once `Page.navigate` resolves, the loader guard always carries the new
loaderId, so `eventLoaderIsRelevant(undefined, guard)` rejected the
commit event outright.
The navigation still completed in the common case only because the
readyState probe short-circuited first. On cross-origin navigations the
probe returns null (the old execution context has been destroyed), so
the wait fell back to `frameNavigated` — which was then rejected — and
`wait_until="commit"` timed out for the full `timeout_ms` instead of
resolving on commit.
Pass `p.frame?.loaderId` through, matching the sibling `lifecycleEvent`
branches. Adds a regression test that makes the readyState probe fail
and asserts commit is reached from the `frameNavigated` event; it fails
(reached "timeout") on the previous code.
Co-Authored-By: Claude <noreply@anthropic.com>
If `chrome.debugger.attach` succeeds but a subsequent CDP domain enable
rejects — most notably `Page.enable`, when the tab navigates to a
chrome:// URL or the Web Store in the gap between the two calls — the
driver left the tab in an unrecoverable state. `attachedTabs` never
recorded the tabId, so the next `ensureAttached` re-attached and failed
permanently with "Another debugger is already attached" until the
extension was reloaded.
Detach the debuggee directly in the failure path (the public `detach()`
is a no-op here because the id is not cached yet) so the next attempt
starts from a clean slate. Adds a regression test that fails on the
previous code, where the rollback never ran and `detach` was never
called.
Co-Authored-By: Claude <noreply@anthropic.com>
Add a popup launcher + action-recording panel that builds a copyable
`bsk record start` prompt, add `--browser` targeting, and default `--url`
to https://example.com/ when omitted. Drop the popup browser-alias field.
Move the reaper's silence timeout and scan cadence into DaemonConfig
(defaulting to the existing 60s/15s constants) with a
`with_browser_liveness` builder, so tests can drive the task in
sub-second windows.
Add an integration test that runs a real daemon and asserts a silent
heartbeat-capable browser — and its sessions — get reaped, while a
legacy browser that never heartbeated is left untouched.
Co-authored-by: Cursor <cursoragent@cursor.com>
Only reap browsers that have sent at least one `system.heartbeat`.
A pre-heartbeat extension never opts in, so it is never dropped on
silence and keeps relying on socket-close detection exactly as before.
This removes a backward-compat regression where a new daemon paired
with an old extension would wrongly reap a live-but-idle connection
and then report `no_browser_connected` on the next command.
- Add a `heartbeat_seen` flag to `BrowserClient`, set when a heartbeat
event arrives, and exclude never-heartbeated clients from
`stale_browsers`.
- Cover both the opt-in and the legacy-survivor cases in unit tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
The manifest now declares a minimum Chrome version and requests the
`idle` permission, and the runtime gains the WebSocket heartbeat and
wake-driven reconnect. Publishing that update requires a new version.
Co-authored-by: Cursor <cursoragent@cursor.com>
The extension emits the literal event string "system.heartbeat";
assert the daemon-side serde name serialises to the same value and
round-trips from the exact frame shape the extension sends, so a
future rename cannot silently break keepalive and liveness.
Co-authored-by: Cursor <cursoragent@cursor.com>
After a long idle the service worker is only revived on its next
keepalive-alarm tick (up to 30s away), so the previous 5s wait made
the first command after idle fail with `no_browser_connected` even
though the extension was about to reconnect.
- Raise the daemon's `EXTENSION_CONNECT_WAIT` (used by `session.start`)
to 35s so it comfortably covers one alarm period plus handshake slack.
- Bump the CLI `session start` IPC timeout to that window plus slack so
it never gives up before the daemon answers, and print a one-line
"waiting for browser extension to connect…" hint when it actually
waits.
- Keep `bsk status` / `browsers` on their own short 5s wait so quick
informational commands are not slowed down.
Co-authored-by: Cursor <cursoragent@cursor.com>
Harden the extension's connection so the daemon link survives normal
idle periods and OS sleep instead of silently dropping.
- Send a `system.heartbeat` event every 20s while the post-handshake
link is live. Since Chrome 116 WebSocket activity resets the MV3
service-worker idle timer, so this keeps the worker (and the socket)
alive during use rather than relying on the 30s keepalive alarm that
sits right on the eviction boundary. Declare `minimum_chrome_version:
"116"` accordingly.
- Reconnect on `runtime.onStartup` and `idle.onStateChanged` ("active"),
which revive the worker after a browser restart or machine wake and
let it reconnect immediately instead of waiting for the next alarm
tick. Add the `idle` permission and document it.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce a `system.heartbeat` protocol event and use it as a
liveness signal on the daemon side.
- Track `last_seen` per connected browser, bumped on every inbound
WS frame (tool response, event, ping/pong, or heartbeat).
- Add a reaper task that drops any browser silent past a 60s window
and purges its sessions, mirroring the WS disconnect cleanup. This
releases half-open connections left behind when the OS resumes from
sleep and kills the service worker without delivering a close frame,
so the daemon no longer pins a phantom "connected" browser and can
idle-exit normally.
- Wire the reaper into both the foreground daemon and the test/embed
entry point, aborting it on shutdown.
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture user actions in the Agent Window and export a record-only
trace (pages[] + navigate/click/fill/select/press steps with target
descriptors). Adds the record overlay UI aligned with the control
overlay, and wires record start/stop/await through CLI, daemon,
protocol, and extension.
Add `bsk network` as a sibling of the `console` command from #7,
reading the buffered network responses / failures for a tab. Fills the
remaining half of the request in #2 (console landed in #7; network did
not).
Mirrors the console command's conventions exactly:
bsk network --session <id> [--tab-id N] [--since C]
[--limit N] [--max-text-chars N]
The extension enables `Network` on attach (best-effort) and buffers
`responseReceived` / `loadingFailed` events per tab with a monotonic
sequence, correlating them to `requestWillBeSent` for method/URL. Reads
are cursor-paginated (`since` -> `next_since`) and bounded (`limit`,
`max_text_chars`) for agent-context safety, same as console. Entries
carry the real HTTP status code (response) or the CDP failure reason
(failure) — things `performance.getEntriesByType('resource')` can't
give.
Classified read-only (pass-through under the pending-interrupt gate);
defaults to the Agent Window's active tab.
Wires bsk-protocol (Method + params/result/entry structs + schema),
bsk-cli (network subcommand + daemon dispatch allow-list) and the
extension (handler + dispatcher route), with unit / parse / IPC tests
paralleling the console command.
Refs #2.
Send borrow-cancel when the confirmation timeout denies, and add a
timer fallback so auto-deny does not depend only on CSS transitionend.
Co-authored-by: Cursor <cursoragent@cursor.com>