browserwright 0.6.9__tar.gz → 0.6.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {browserwright-0.6.9 → browserwright-0.6.12}/PKG-INFO +1 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/README.md +36 -54
- {browserwright-0.6.9 → browserwright-0.6.12}/pyproject.toml +1 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/_executor/client.py +7 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/config.py +13 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/extension_upstream.py +7 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/facade_extension.py +2 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/listener.py +91 -6
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/proxy.py +3 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/relay.py +27 -2
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/page.py +11 -3
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/session_create.py +9 -2
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/session_registry.py +14 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/PKG-INFO +1 -1
- {browserwright-0.6.9 → browserwright-0.6.12}/setup.cfg +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/__main__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/_executor/process.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/_executor/protocol.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/api.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/cdp.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/cli.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/_ipc.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/active_tab.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/auth.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/base.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/cloud.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/env.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/extension.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/backends/rdp.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/cli.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/doctor.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/launch_chrome.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/observability.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/platforms.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/resolver.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/daemon.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/executor_registry.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/facade.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/state.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/upstream.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/discovery.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/errors.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/health.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/install.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/_yaml.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/global_mem.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/repl_mem.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/session_decisions.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/memory/site_mem.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/mode_b_client.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/multitask.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/inspect.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/interact.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/primitives/site.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/release_install.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/_smart_goto.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/inline.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/playwright_handle.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/session.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/session_runtime.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/skill_doc.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/skill_runtime.md +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/subscriptions.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/task_runner.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/version.py +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/SOURCES.txt +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/requires.txt +0 -0
- {browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.12
|
|
4
4
|
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: cdp-use==1.4.5
|
|
@@ -61,13 +61,12 @@ from your own systemd-user unit or process supervisor.
|
|
|
61
61
|
|
|
62
62
|
The browser extension is intentionally not part of the PyPI packaging contract.
|
|
63
63
|
The long-term user path is to publish/install it through the Chrome Web Store.
|
|
64
|
-
Until then, local development
|
|
65
|
-
|
|
64
|
+
Until then, local development can use the repo's `chrome-extension/` directory,
|
|
65
|
+
and global installs should use the GitHub Release extension artifact described
|
|
66
66
|
below.
|
|
67
67
|
|
|
68
|
-
The agent skill bundle remains a thin shell around the installed CLI. For
|
|
69
|
-
|
|
70
|
-
Code, Codex, and Pi. For PyPI installs, the intended stable contract is:
|
|
68
|
+
The agent skill bundle remains a thin shell around the installed CLI. For PyPI
|
|
69
|
+
installs, the intended stable contract is:
|
|
71
70
|
|
|
72
71
|
```bash
|
|
73
72
|
browserwright --print-skill
|
|
@@ -115,60 +114,39 @@ git push origin v0.6.3
|
|
|
115
114
|
The GitHub Action only runs for `v*` tags. A tag like `v0.6.3` publishes package
|
|
116
115
|
version `0.6.3`.
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
Each `v*` GitHub Release also includes a
|
|
118
|
+
`browserwright-extension-<version>.zip` asset. Use that zip as the official
|
|
119
|
+
extension download path when installing outside PyPI or before the Chrome Web
|
|
120
|
+
Store path is available.
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
### Local development link
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
This builds a wheel, installs it into:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
~/.local/share/browserwright/releases/<version>/
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
and points global entry points at that release copy:
|
|
124
|
+
For development only, `mise run dev-link` links the current checkout into
|
|
125
|
+
global PATH and skill dirs. That is convenient for debugging but can break
|
|
126
|
+
global agents if the checkout is broken.
|
|
133
127
|
|
|
134
128
|
```bash
|
|
135
|
-
|
|
136
|
-
~/.local/bin/browserwright-daemon -> releases/<version>/.venv/bin/browserwright-daemon
|
|
137
|
-
~/.claude/skills/browserwright -> releases/<version>/skill
|
|
138
|
-
~/.codex/skills/browserwright -> releases/<version>/skill
|
|
139
|
-
~/.pi/agent/skills/browserwright -> releases/<version>/skill
|
|
129
|
+
mise run dev-link
|
|
140
130
|
```
|
|
141
131
|
|
|
142
|
-
The
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
browserwright release status
|
|
148
|
-
browserwright release list
|
|
149
|
-
browserwright release activate <version>
|
|
150
|
-
mise run version-check
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
The skill bundle is intentionally a thin shell. The authoritative agent instructions come from `browserwright --print-skill`, so installed agents read docs generated by the installed CLI in the active release.
|
|
154
|
-
|
|
155
|
-
For development only, `mise run dev-link` links the current checkout into global PATH and skill dirs. That is convenient for debugging but can break global agents if the checkout is broken.
|
|
132
|
+
The skill bundle is intentionally a thin shell. The authoritative agent
|
|
133
|
+
instructions come from `browserwright --print-skill`, so installed agents read
|
|
134
|
+
docs generated by the installed CLI.
|
|
156
135
|
|
|
157
136
|
## Update the local global install
|
|
158
137
|
|
|
159
|
-
Use this flow when you want the global Code Agent install on this machine to
|
|
138
|
+
Use this flow when you want the global Code Agent install on this machine to
|
|
139
|
+
pick up the latest published release:
|
|
160
140
|
|
|
161
141
|
```bash
|
|
162
|
-
# 1.
|
|
163
|
-
|
|
142
|
+
# 1. Install/upgrade browserwright from PyPI.
|
|
143
|
+
# 2. Download the matching GitHub Release extension artifact.
|
|
144
|
+
# 3. Restart the daemon and verify versions.
|
|
145
|
+
mise run upgrade-global
|
|
164
146
|
|
|
165
|
-
#
|
|
166
|
-
browserwright release status
|
|
147
|
+
# 4. Optional explicit verification.
|
|
167
148
|
browserwright version check
|
|
168
149
|
browserwright-daemon version check
|
|
169
|
-
|
|
170
|
-
# 3. Restart the daemon if release status says the running daemon is stale.
|
|
171
|
-
browserwright-daemon restart
|
|
172
150
|
```
|
|
173
151
|
|
|
174
152
|
If the daemon is running manually instead of as the macOS LaunchAgent, restart it manually:
|
|
@@ -178,23 +156,25 @@ browserwright-daemon stop
|
|
|
178
156
|
browserwright-daemon serve
|
|
179
157
|
```
|
|
180
158
|
|
|
181
|
-
|
|
159
|
+
`upgrade-global` unpacks the GitHub Release asset into the stable local load
|
|
160
|
+
path:
|
|
182
161
|
|
|
183
162
|
```bash
|
|
184
163
|
/Users/metajs/Library/Mobile Documents/com~apple~CloudDocs/etc/chrome-extension/browserwright
|
|
185
164
|
```
|
|
186
165
|
|
|
187
|
-
If
|
|
166
|
+
If the task reports that the extension changed, open `chrome://extensions/` and
|
|
167
|
+
reload the `browserwright` unpacked extension from that stable path. The path
|
|
168
|
+
does not change across releases; the task overwrites its contents.
|
|
188
169
|
Reload any existing tab that already shows a duplicated `👀` attach marker so
|
|
189
170
|
the extension can normalize the title marker after the upgrade.
|
|
190
171
|
|
|
191
|
-
##
|
|
172
|
+
## Release discipline
|
|
192
173
|
|
|
193
|
-
For
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
checks all read or compare against the built package version.
|
|
174
|
+
For PyPI releases, the git tag is the source of truth and the publish workflow
|
|
175
|
+
writes that version into `pyproject.toml` before building. The release workflow
|
|
176
|
+
also writes the same tag version into `chrome-extension/manifest.json` before
|
|
177
|
+
uploading `browserwright-extension-<version>.zip` to the GitHub Release.
|
|
198
178
|
|
|
199
179
|
After installing a release:
|
|
200
180
|
|
|
@@ -204,7 +184,9 @@ browserwright-daemon version check
|
|
|
204
184
|
browserwright-daemon restart # when installed as the macOS LaunchAgent
|
|
205
185
|
```
|
|
206
186
|
|
|
207
|
-
|
|
187
|
+
Chrome does not allow this repo to refresh an unpacked extension automatically.
|
|
188
|
+
If `mise run upgrade-global` reports that the extension changed, reload it in
|
|
189
|
+
Chrome.
|
|
208
190
|
|
|
209
191
|
## Smoke test
|
|
210
192
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browserwright"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.12"
|
|
8
8
|
description = "Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends)."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
@@ -17,6 +17,7 @@ import socket
|
|
|
17
17
|
import time
|
|
18
18
|
|
|
19
19
|
from ..errors import BrowserwrightError
|
|
20
|
+
from .. import session_registry as reg
|
|
20
21
|
from .protocol import (
|
|
21
22
|
DEFAULT_TIMEOUT_MS,
|
|
22
23
|
ExecuteRequest,
|
|
@@ -81,6 +82,12 @@ def run_on_executor(sess, code: str, *,
|
|
|
81
82
|
connect_over_cdp + bind (moved off the control plane), which can add up to
|
|
82
83
|
~35s. The executor itself bounds the worker per-call timeout; this slack
|
|
83
84
|
only prevents the CLIENT recv from giving up before the executor replies."""
|
|
85
|
+
sid = _session_id(sess)
|
|
86
|
+
# Session idle is "time since the last user/agent instruction", not
|
|
87
|
+
# executor process liveness. Touch before contacting the executor so a
|
|
88
|
+
# wedged or long-running executor cannot prevent the durable idle clock
|
|
89
|
+
# from reflecting that a new instruction arrived.
|
|
90
|
+
reg.touch(sid)
|
|
84
91
|
sock_path = ensure_executor(sess)
|
|
85
92
|
recv_timeout = max(timeout_ms, 1) / 1000.0 + _COLD_START_RECV_SLACK_S
|
|
86
93
|
conn = _connect(sock_path, timeout=recv_timeout)
|
|
@@ -131,6 +131,7 @@ class Config:
|
|
|
131
131
|
cdp_url: str | None = None # BD_CDP_URL / BU_CDP_URL — env backend uses this
|
|
132
132
|
chrome_binary: str | None = None # BD_CHROME_BINARY — launch-chrome
|
|
133
133
|
idle_close_after: float | None = None # seconds; None = never (default)
|
|
134
|
+
session_idle_prune: float | None = 24 * 3600 # ledger prune threshold
|
|
134
135
|
# Playwright-facing CDP facade. `serve` binds an additional TCP ws+HTTP
|
|
135
136
|
# endpoint that a real Playwright client can `connect_over_cdp` to.
|
|
136
137
|
#
|
|
@@ -246,6 +247,9 @@ def load(
|
|
|
246
247
|
cfg.backends.cloud.auth = dict(auth_subtables[cfg.backends.cloud.auth_kind])
|
|
247
248
|
if "idle_close_after" in toml and isinstance(toml["idle_close_after"], (int, float)):
|
|
248
249
|
cfg.idle_close_after = float(toml["idle_close_after"])
|
|
250
|
+
if "session_idle_prune" in toml and isinstance(toml["session_idle_prune"], (int, float)):
|
|
251
|
+
v = float(toml["session_idle_prune"])
|
|
252
|
+
cfg.session_idle_prune = v if v > 0 else None
|
|
249
253
|
# Playwright facade port (phase A1). toml key `facade_port`.
|
|
250
254
|
if "facade_port" in toml and isinstance(toml["facade_port"], int):
|
|
251
255
|
cfg.facade_port = toml["facade_port"]
|
|
@@ -268,6 +272,15 @@ def load(
|
|
|
268
272
|
from .errors import UserError
|
|
269
273
|
raise UserError(
|
|
270
274
|
f"BD_IDLE_CLOSE_AFTER must be a number, got {e['BD_IDLE_CLOSE_AFTER']!r}")
|
|
275
|
+
if "BD_SESSION_IDLE_PRUNE" in e:
|
|
276
|
+
try:
|
|
277
|
+
v = float(e["BD_SESSION_IDLE_PRUNE"])
|
|
278
|
+
cfg.session_idle_prune = v if v > 0 else None
|
|
279
|
+
except ValueError:
|
|
280
|
+
from .errors import UserError
|
|
281
|
+
raise UserError(
|
|
282
|
+
f"BD_SESSION_IDLE_PRUNE must be a number, got "
|
|
283
|
+
f"{e['BD_SESSION_IDLE_PRUNE']!r}")
|
|
271
284
|
if "BD_CDP_WS" in e:
|
|
272
285
|
cfg.cdp_ws = e["BD_CDP_WS"]; cfg.cdp_ws_source = "BD_CDP_WS"
|
|
273
286
|
elif "BU_CDP_WS" in e:
|
{browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/extension_upstream.py
RENAMED
|
@@ -500,6 +500,7 @@ class ExtensionUpstream:
|
|
|
500
500
|
group_name: str | None = "Agent",
|
|
501
501
|
session_id: str | None = None,
|
|
502
502
|
background: bool = True,
|
|
503
|
+
skip_post_attach_commands: bool = False,
|
|
503
504
|
) -> dict:
|
|
504
505
|
"""Open a background tab in the session's tab group via the relay,
|
|
505
506
|
fabricate a sessionId, and return
|
|
@@ -514,7 +515,12 @@ class ExtensionUpstream:
|
|
|
514
515
|
gid = self._relay.session_group(session_id)
|
|
515
516
|
self.reset_session_announce(session_id)
|
|
516
517
|
gt = await self._relay.create_background_tab(
|
|
517
|
-
url,
|
|
518
|
+
url,
|
|
519
|
+
group_name=group_name,
|
|
520
|
+
group_id=gid,
|
|
521
|
+
background=background,
|
|
522
|
+
skip_post_attach_commands=skip_post_attach_commands,
|
|
523
|
+
)
|
|
518
524
|
group_id = getattr(gt, "group_id", -1)
|
|
519
525
|
group_id = int(group_id) if isinstance(group_id, int) else -1
|
|
520
526
|
if self._group_required(
|
{browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/facade_extension.py
RENAMED
|
@@ -511,7 +511,8 @@ class ExtensionFacadeBridge:
|
|
|
511
511
|
gt = await self._ext.open_background_tab(
|
|
512
512
|
url, group_name=group_name,
|
|
513
513
|
session_id=self._session_id,
|
|
514
|
-
background=True
|
|
514
|
+
background=True,
|
|
515
|
+
skip_post_attach_commands=True)
|
|
515
516
|
tab_id = int(gt["tabId"])
|
|
516
517
|
created_group = gt.get("groupId")
|
|
517
518
|
if isinstance(created_group, int) and created_group >= 0:
|
|
@@ -46,6 +46,8 @@ from .facade import PlaywrightFacade
|
|
|
46
46
|
|
|
47
47
|
logger = logging.getLogger(__name__)
|
|
48
48
|
|
|
49
|
+
_SESSION_PRUNE_INTERVAL_S = 3600.0
|
|
50
|
+
|
|
49
51
|
|
|
50
52
|
# ---- per-upstream context factory ------------------------------------------
|
|
51
53
|
|
|
@@ -224,8 +226,10 @@ async def run_serve(cfg: Config) -> int:
|
|
|
224
226
|
# (Fork 4 self-exit / segfault) so the registry never accumulates corpses.
|
|
225
227
|
# Upstream idle-close + executor idle-reap are gated on cfg.idle_close_after
|
|
226
228
|
# inside the loop.
|
|
229
|
+
await _auto_prune_sessions(daemon, reason="startup")
|
|
227
230
|
idle_task: asyncio.Task | None = asyncio.create_task(
|
|
228
|
-
_idle_watchdog(daemon, cfg.idle_close_after
|
|
231
|
+
_idle_watchdog(daemon, cfg.idle_close_after,
|
|
232
|
+
session_idle_prune=cfg.session_idle_prune))
|
|
229
233
|
try:
|
|
230
234
|
await stop.wait()
|
|
231
235
|
logger.info("browserwright-daemon shutdown requested")
|
|
@@ -979,7 +983,76 @@ class _UpstreamHolder:
|
|
|
979
983
|
# ---- graceful shutdown -----------------------------------------------------
|
|
980
984
|
|
|
981
985
|
|
|
982
|
-
async def
|
|
986
|
+
async def _auto_prune_sessions(daemon: "Daemon", *, reason: str) -> list[dict]:
|
|
987
|
+
"""Best-effort durable ledger prune.
|
|
988
|
+
|
|
989
|
+
The session idle clock is `ledger.last_seen`, updated when a new
|
|
990
|
+
user/agent instruction arrives. Executor liveness is deliberately ignored:
|
|
991
|
+
a stuck executor can stay alive forever and must not keep a session from
|
|
992
|
+
being cleaned after the instruction-idle threshold.
|
|
993
|
+
"""
|
|
994
|
+
idle_seconds = daemon.cfg.session_idle_prune
|
|
995
|
+
if not idle_seconds:
|
|
996
|
+
return []
|
|
997
|
+
try:
|
|
998
|
+
from ... import session_registry
|
|
999
|
+
stale = session_registry.stale(idle_seconds=idle_seconds)
|
|
1000
|
+
except Exception as e: # noqa: BLE001 - cleanup must not kill the daemon
|
|
1001
|
+
logger.warning("auto session-prune failed (%s): %r", reason, e)
|
|
1002
|
+
return []
|
|
1003
|
+
pruned: list[dict] = []
|
|
1004
|
+
for rec in stale:
|
|
1005
|
+
sid = str(rec.get("id") or "")
|
|
1006
|
+
if not sid:
|
|
1007
|
+
continue
|
|
1008
|
+
try:
|
|
1009
|
+
daemon.executors.kill(sid)
|
|
1010
|
+
except Exception as e: # noqa: BLE001
|
|
1011
|
+
logger.warning("auto session-prune executor kill failed "
|
|
1012
|
+
"(session=%s): %r", sid, e)
|
|
1013
|
+
if rec.get("backend") == "rdp" and rec.get("owner") == "create":
|
|
1014
|
+
try:
|
|
1015
|
+
await daemon.teardown_rdp_context(sid)
|
|
1016
|
+
except Exception as e: # noqa: BLE001
|
|
1017
|
+
logger.warning("auto session-prune rdp teardown failed "
|
|
1018
|
+
"(session=%s): %r", sid, e)
|
|
1019
|
+
elif rec.get("backend") == "extension":
|
|
1020
|
+
try:
|
|
1021
|
+
runtime = rec.get("runtime") or {}
|
|
1022
|
+
group_id = runtime.get("group_id")
|
|
1023
|
+
group_id = (
|
|
1024
|
+
group_id
|
|
1025
|
+
if isinstance(group_id, int) and group_id >= 0
|
|
1026
|
+
else None
|
|
1027
|
+
)
|
|
1028
|
+
holder = daemon.shared_context.holder
|
|
1029
|
+
upstream = holder.upstream
|
|
1030
|
+
if hasattr(upstream, "end_session"):
|
|
1031
|
+
await upstream.end_session(sid, group_id) # type: ignore[attr-defined]
|
|
1032
|
+
except Exception as e: # noqa: BLE001
|
|
1033
|
+
logger.warning("auto session-prune extension teardown failed "
|
|
1034
|
+
"(session=%s): %r", sid, e)
|
|
1035
|
+
try:
|
|
1036
|
+
removed = session_registry.remove(sid)
|
|
1037
|
+
except Exception as e: # noqa: BLE001
|
|
1038
|
+
logger.warning("auto session-prune ledger remove failed "
|
|
1039
|
+
"(session=%s): %r", sid, e)
|
|
1040
|
+
removed = None
|
|
1041
|
+
if removed is not None:
|
|
1042
|
+
pruned.append(removed)
|
|
1043
|
+
if pruned:
|
|
1044
|
+
logger.info("auto-pruned %d idle session(s) on %s: %s",
|
|
1045
|
+
len(pruned), reason,
|
|
1046
|
+
[str(rec.get("id")) for rec in pruned])
|
|
1047
|
+
return pruned
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
async def _idle_watchdog(
|
|
1051
|
+
daemon: "Daemon",
|
|
1052
|
+
idle_after: float | None,
|
|
1053
|
+
*,
|
|
1054
|
+
session_idle_prune: float | None = None,
|
|
1055
|
+
) -> None:
|
|
983
1056
|
"""Spec §6.5/§6.6: when configured, close each upstream after `idle_after`
|
|
984
1057
|
seconds with no activity. The next client command lazy-opens it again.
|
|
985
1058
|
|
|
@@ -995,13 +1068,21 @@ async def _idle_watchdog(daemon: "Daemon", idle_after: float | None) -> None:
|
|
|
995
1068
|
leak a subprocess.
|
|
996
1069
|
|
|
997
1070
|
Runs unconditionally; idle-close + idle-reap are no-ops when `idle_after`
|
|
998
|
-
is None.
|
|
999
|
-
|
|
1071
|
+
is None. Durable ledger prune is controlled independently by
|
|
1072
|
+
`session_idle_prune`. We poll at the smallest active supervision cadence,
|
|
1073
|
+
or every 5s when only crash-reap is active.
|
|
1000
1074
|
"""
|
|
1001
|
-
|
|
1075
|
+
cadences = [5.0]
|
|
1076
|
+
if idle_after:
|
|
1077
|
+
cadences.append(max(1.0, idle_after / 2.0))
|
|
1078
|
+
if session_idle_prune:
|
|
1079
|
+
cadences.append(_SESSION_PRUNE_INTERVAL_S)
|
|
1080
|
+
poll = min(cadences)
|
|
1081
|
+
last_session_prune_at = time.time()
|
|
1002
1082
|
try:
|
|
1003
1083
|
while True:
|
|
1004
1084
|
await asyncio.sleep(poll)
|
|
1085
|
+
now = time.time()
|
|
1005
1086
|
# --- executor supervision (Phase B PR2) ---
|
|
1006
1087
|
try:
|
|
1007
1088
|
daemon.executors.reap_dead()
|
|
@@ -1010,12 +1091,16 @@ async def _idle_watchdog(daemon: "Daemon", idle_after: float | None) -> None:
|
|
|
1010
1091
|
except Exception as e: # noqa: BLE001 - never let reap break the loop
|
|
1011
1092
|
logger.warning("executor reap failed: %r", e)
|
|
1012
1093
|
# --- upstream idle-close (gated) ---
|
|
1094
|
+
if session_idle_prune and (
|
|
1095
|
+
now - last_session_prune_at >= _SESSION_PRUNE_INTERVAL_S):
|
|
1096
|
+
await _auto_prune_sessions(daemon, reason="watchdog")
|
|
1097
|
+
last_session_prune_at = now
|
|
1013
1098
|
if not idle_after:
|
|
1014
1099
|
continue
|
|
1015
1100
|
for ctx in daemon.all_contexts():
|
|
1016
1101
|
if ctx.state.upstream_phase != UpstreamPhase.CONNECTED:
|
|
1017
1102
|
continue
|
|
1018
|
-
idle_for =
|
|
1103
|
+
idle_for = now - ctx.state.last_activity_at
|
|
1019
1104
|
if idle_for >= idle_after:
|
|
1020
1105
|
logger.info("idle-watchdog: closing %s upstream after %.1fs",
|
|
1021
1106
|
ctx.backend, idle_for)
|
|
@@ -1171,10 +1171,12 @@ class Router:
|
|
|
1171
1171
|
# extension backend; background=False opens the tab in the foreground.
|
|
1172
1172
|
background = params.get("background")
|
|
1173
1173
|
background = background if isinstance(background, bool) else True
|
|
1174
|
+
skip_post_attach_commands = params.get("skipPostAttachCommands") is True
|
|
1174
1175
|
try:
|
|
1175
1176
|
result = await self._open_background_tab(
|
|
1176
1177
|
url, group_name=group_name, session_id=session,
|
|
1177
|
-
background=background
|
|
1178
|
+
background=background,
|
|
1179
|
+
skip_post_attach_commands=skip_post_attach_commands)
|
|
1178
1180
|
except Exception as e:
|
|
1179
1181
|
await self._send_to_client(client.client_id, _error_response(
|
|
1180
1182
|
req_id, -32603, f"openBackgroundTab failed: {e!r}"))
|
|
@@ -435,6 +435,7 @@ class RelayServer:
|
|
|
435
435
|
group_name: str | None = "Agent",
|
|
436
436
|
group_id: int | None = None,
|
|
437
437
|
background: bool = True,
|
|
438
|
+
skip_post_attach_commands: bool = False,
|
|
438
439
|
timeout: float = 10.0,
|
|
439
440
|
) -> GhostTarget:
|
|
440
441
|
"""Spec Phase B Feature 1: open a tab in the background (active=false)
|
|
@@ -467,6 +468,8 @@ class RelayServer:
|
|
|
467
468
|
# is requested so existing extensions default to background.
|
|
468
469
|
if not background:
|
|
469
470
|
body["background"] = False
|
|
471
|
+
if skip_post_attach_commands:
|
|
472
|
+
body["skipPostAttachCommands"] = True
|
|
470
473
|
result = await self._request(ext, body, timeout=timeout) or {}
|
|
471
474
|
tab_id = int(result.get("tabId", -1))
|
|
472
475
|
if tab_id < 0:
|
|
@@ -866,7 +869,7 @@ class RelayServer:
|
|
|
866
869
|
# lifecycle so they can synthesize Target.targetCreated /
|
|
867
870
|
# attachedToTarget for a live `connect_over_cdp` client. The agent
|
|
868
871
|
# path ignores these (its `_on_event` only handles `event`).
|
|
869
|
-
|
|
872
|
+
self._schedule_fanout_listeners(msg)
|
|
870
873
|
return
|
|
871
874
|
|
|
872
875
|
if kind == "detached":
|
|
@@ -874,7 +877,7 @@ class RelayServer:
|
|
|
874
877
|
if tab_id < 0:
|
|
875
878
|
return
|
|
876
879
|
ext.tabs.pop(tab_id, None)
|
|
877
|
-
|
|
880
|
+
self._schedule_fanout_listeners(msg)
|
|
878
881
|
return
|
|
879
882
|
|
|
880
883
|
if kind == "response":
|
|
@@ -933,6 +936,28 @@ class RelayServer:
|
|
|
933
936
|
except Exception as e: # noqa: BLE001
|
|
934
937
|
logger.warning("relay fan-out listener raised: %r", e)
|
|
935
938
|
|
|
939
|
+
def _schedule_fanout_listeners(self, msg: dict) -> None:
|
|
940
|
+
"""Notify secondary observers without blocking the relay reader.
|
|
941
|
+
|
|
942
|
+
An extension ``attached`` frame is often followed immediately by the
|
|
943
|
+
response to the create/attach request. Facade listeners may issue their
|
|
944
|
+
own relay requests for scoped visibility checks, so awaiting them inline
|
|
945
|
+
can deadlock the single websocket reader before it consumes the pending
|
|
946
|
+
response. Schedule the fan-out instead and keep draining extension
|
|
947
|
+
frames.
|
|
948
|
+
"""
|
|
949
|
+
if not self._event_listeners:
|
|
950
|
+
return
|
|
951
|
+
task = asyncio.create_task(self._fanout_listeners(dict(msg)))
|
|
952
|
+
|
|
953
|
+
def _done(t: asyncio.Task) -> None:
|
|
954
|
+
try:
|
|
955
|
+
t.result()
|
|
956
|
+
except Exception as e: # noqa: BLE001
|
|
957
|
+
logger.warning("relay fan-out task raised: %r", e)
|
|
958
|
+
|
|
959
|
+
task.add_done_callback(_done)
|
|
960
|
+
|
|
936
961
|
|
|
937
962
|
class _CommandError(Exception):
|
|
938
963
|
"""Wrapped extension-side CDP error. Surfaced to the caller in
|
|
@@ -224,7 +224,12 @@ def switch_tab(target) -> dict:
|
|
|
224
224
|
return {"targetId": target_id}
|
|
225
225
|
|
|
226
226
|
|
|
227
|
-
def open(
|
|
227
|
+
def open(
|
|
228
|
+
url: str = "about:blank",
|
|
229
|
+
*,
|
|
230
|
+
background: bool = True,
|
|
231
|
+
skip_post_attach_commands: bool = False,
|
|
232
|
+
) -> dict:
|
|
228
233
|
"""Open a new working tab in this session's browser, attach, bind as
|
|
229
234
|
current. The unified tab-opening primitive (docs §Tier B) — replaces
|
|
230
235
|
both ``new_tab`` and ``open_background``.
|
|
@@ -246,7 +251,10 @@ def open(url: str = "about:blank", *, background: bool = True) -> dict:
|
|
|
246
251
|
try:
|
|
247
252
|
payload = sess.cdp.send(
|
|
248
253
|
"BrowserwrightDaemon.openBackgroundTab",
|
|
249
|
-
url=url,
|
|
254
|
+
url=url,
|
|
255
|
+
bsSession=sid,
|
|
256
|
+
background=background,
|
|
257
|
+
skipPostAttachCommands=skip_post_attach_commands,
|
|
250
258
|
)
|
|
251
259
|
except CDPError as e:
|
|
252
260
|
raise CDPError(
|
|
@@ -400,7 +408,7 @@ def current_page() -> dict:
|
|
|
400
408
|
return {"targetId": tabs[0]["targetId"], "url": tabs[0]["url"],
|
|
401
409
|
"title": tabs[0]["title"], "accuracy": "unknown"}
|
|
402
410
|
# 4. Empty session — open a fresh working tab (NOT adopt).
|
|
403
|
-
return open("about:blank") | {"accuracy": "unknown"}
|
|
411
|
+
return open("about:blank", skip_post_attach_commands=True) | {"accuracy": "unknown"}
|
|
404
412
|
|
|
405
413
|
|
|
406
414
|
def wait(seconds: float = 1.0) -> None:
|
|
@@ -125,10 +125,17 @@ def reset_executor(record: dict) -> str:
|
|
|
125
125
|
def reap(*, idle_seconds: float) -> list[dict]:
|
|
126
126
|
"""Prune idle sessions; for create-owned ones, also tear down the browser
|
|
127
127
|
the daemon launched. Returns the pruned records."""
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
stale = reg.stale(idle_seconds=idle_seconds)
|
|
129
|
+
pruned: list[dict] = []
|
|
130
|
+
for rec in stale:
|
|
131
|
+
_reap_executor(rec)
|
|
132
|
+
if rec.get("backend") == "extension":
|
|
133
|
+
_end_extension_workspace(rec)
|
|
130
134
|
if rec.get("owner") == "create":
|
|
131
135
|
_close_browser(rec)
|
|
136
|
+
removed = reg.remove(str(rec.get("id")))
|
|
137
|
+
if removed is not None:
|
|
138
|
+
pruned.append(removed)
|
|
132
139
|
return pruned
|
|
133
140
|
|
|
134
141
|
|
|
@@ -124,6 +124,20 @@ def list_all() -> list[dict]:
|
|
|
124
124
|
return [sessions[k] for k in sorted(sessions, key=int)]
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def stale(*, idle_seconds: float) -> list[dict]:
|
|
128
|
+
"""Sessions idle longer than ``idle_seconds`` without removing them."""
|
|
129
|
+
now = time.time()
|
|
130
|
+
p = _ledger_path()
|
|
131
|
+
if not p.exists():
|
|
132
|
+
return []
|
|
133
|
+
sessions = json.loads(p.read_text())["sessions"]
|
|
134
|
+
records = [
|
|
135
|
+
e for e in sessions.values()
|
|
136
|
+
if now - e.get("last_seen", 0.0) >= idle_seconds
|
|
137
|
+
]
|
|
138
|
+
return sorted(records, key=lambda e: int(e.get("id", 0)))
|
|
139
|
+
|
|
140
|
+
|
|
127
141
|
def prune(*, idle_seconds: float) -> list[dict]:
|
|
128
142
|
"""Remove sessions idle longer than ``idle_seconds``; return removed records."""
|
|
129
143
|
now = time.time()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.12
|
|
4
4
|
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: cdp-use==1.4.5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright/daemon/server/executor_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{browserwright-0.6.9 → browserwright-0.6.12}/src/browserwright.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|