browserwright 0.7.4__tar.gz → 0.8.0__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.
Files changed (96) hide show
  1. {browserwright-0.7.4 → browserwright-0.8.0}/PKG-INFO +1 -1
  2. {browserwright-0.7.4 → browserwright-0.8.0}/pyproject.toml +1 -1
  3. browserwright-0.8.0/src/browserwright/_executor/client.py +301 -0
  4. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/process.py +241 -157
  5. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/protocol.py +139 -3
  6. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/cli.py +163 -43
  7. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/_ipc.py +46 -6
  8. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/cli.py +19 -6
  9. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/executor_registry.py +126 -15
  10. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/verbs.py +30 -5
  11. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/errors.py +31 -3
  12. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/inline.py +13 -7
  13. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/playwright_handle.py +167 -40
  14. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session.py +20 -0
  15. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_create.py +16 -1
  16. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/skill_runtime.md +50 -8
  17. browserwright-0.8.0/src/browserwright/task_runner.py +348 -0
  18. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/PKG-INFO +1 -1
  19. browserwright-0.7.4/src/browserwright/_executor/client.py +0 -135
  20. browserwright-0.7.4/src/browserwright/task_runner.py +0 -134
  21. {browserwright-0.7.4 → browserwright-0.8.0}/README.md +0 -0
  22. {browserwright-0.7.4 → browserwright-0.8.0}/setup.cfg +0 -0
  23. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/__init__.py +0 -0
  24. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/__main__.py +0 -0
  25. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/__init__.py +0 -0
  26. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/__main__.py +0 -0
  27. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/api.py +0 -0
  28. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/cdp.py +0 -0
  29. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/__init__.py +0 -0
  30. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/_stale.py +0 -0
  31. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/__init__.py +0 -0
  32. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/base.py +0 -0
  33. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/cdp.py +0 -0
  34. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/extension.py +0 -0
  35. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/config.py +0 -0
  36. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/doctor.py +0 -0
  37. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/errors.py +0 -0
  38. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/launch_chrome.py +0 -0
  39. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/observability.py +0 -0
  40. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/platforms.py +0 -0
  41. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/resolver.py +0 -0
  42. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/__init__.py +0 -0
  43. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/daemon.py +0 -0
  44. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/extension_upstream.py +0 -0
  45. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/facade.py +0 -0
  46. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/facade_extension.py +0 -0
  47. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/listener.py +0 -0
  48. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/proxy.py +0 -0
  49. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/relay.py +0 -0
  50. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/state.py +0 -0
  51. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/upstream.py +0 -0
  52. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/userscripts.py +0 -0
  53. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/discovery.py +0 -0
  54. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/health.py +0 -0
  55. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/install.py +0 -0
  56. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/__init__.py +0 -0
  57. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_lock.py +0 -0
  58. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_md.py +0 -0
  59. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_yaml.py +0 -0
  60. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/global_mem.py +0 -0
  61. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/site_mem.py +0 -0
  62. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/mode_b_client.py +0 -0
  63. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/output_schema.py +0 -0
  64. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/__init__.py +0 -0
  65. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/discovery_api.py +0 -0
  66. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/http.py +0 -0
  67. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/site.py +0 -0
  68. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/__init__.py +0 -0
  69. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/_namespace.py +0 -0
  70. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/_smart_goto.py +0 -0
  71. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/snapshot.py +0 -0
  72. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_ctx.py +0 -0
  73. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_registry.py +0 -0
  74. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_runtime.py +0 -0
  75. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
  76. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
  77. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
  78. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
  79. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
  80. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
  81. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
  82. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
  83. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
  84. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
  85. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
  86. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
  87. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
  88. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
  89. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
  90. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/skill_doc.py +0 -0
  91. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/version.py +0 -0
  92. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/SOURCES.txt +0 -0
  93. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/dependency_links.txt +0 -0
  94. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/entry_points.txt +0 -0
  95. {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/requires.txt +0 -0
  96. {browserwright-0.7.4 → browserwright-0.8.0}/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.7.4
3
+ Version: 0.8.0
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: websockets==15.0.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "browserwright"
7
- version = "0.7.4"
7
+ version = "0.8.0"
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 = [
@@ -0,0 +1,301 @@
1
+ """Thin-client side of the executor data plane.
2
+
3
+ Used by ``repl/inline.py`` when inline code touches ``page`` / ``context`` /
4
+ ``snapshot`` / ``state`` / ``reset``: the whole code body is shipped to the
5
+ session's resident executor and the response is replayed locally.
6
+
7
+ Control plane (spawn + discover) goes through the daemon's
8
+ ``BrowserwrightDaemon.ensureExecutor`` verb over the EXISTING mode_b socket
9
+ (tiny payload). The daemon spawns the executor if absent, waits for it to bind +
10
+ write its ``_ipc`` discovery file, and returns the socket path. The data plane
11
+ (this module) then connects DIRECTLY to that socket — keeping arbitrary code +
12
+ large output off the daemon's event loop (Fork 2).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import signal
17
+ import socket
18
+ import threading
19
+ import time
20
+ from collections.abc import Iterator
21
+ from contextlib import contextmanager
22
+ from dataclasses import dataclass
23
+
24
+ from .. import session_registry as reg
25
+ from ..errors import BrowserwrightError
26
+ from .protocol import (
27
+ DEFAULT_TIMEOUT_MS,
28
+ ExecuteRequest,
29
+ ExecuteResponse,
30
+ TaskEnvelope,
31
+ recv_message,
32
+ send_message,
33
+ )
34
+
35
+ # Allow the executor's deadline response a small framing/delivery margin. The
36
+ # browser cold-start is part of the request deadline, not hidden extra time.
37
+ _RESPONSE_DELIVERY_SLACK_S = 2.0
38
+
39
+
40
+ class ExecutorUnavailable(BrowserwrightError):
41
+ """The session's executor could not be ensured/reached.
42
+
43
+ Surfaced when ``ensureExecutor`` fails or the executor socket can't be
44
+ connected — actionable: the daemon must be running (it spawns the
45
+ executor)."""
46
+
47
+ default_fix = ("ensure the daemon is running (`browserwright-daemon status "
48
+ "--json` should show `alive`); if the executor is stale, "
49
+ "call `reset()` as a standalone/final inline statement, "
50
+ "then retry in a new command; or run `browserwright "
51
+ "session reset <id>` before retrying.")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class ExecutorLease:
56
+ sock_path: str
57
+ executor_id: str
58
+
59
+
60
+ def _ensure_executor_lease(sess) -> ExecutorLease:
61
+ """Ask the daemon to ensure the session's executor and return its socket
62
+ path. Uses the session's mode_b CDP client (``sess.cdp``) to send the
63
+ control-plane verb."""
64
+ sid = _session_id(sess)
65
+ try:
66
+ # The browserwright session is already bound on the websocket query
67
+ # (`?session=<id>`). Do not pass it as CDP's top-level `sessionId`;
68
+ # that field means "attached target session" inside the proxy mux.
69
+ res = sess.cdp.send("BrowserwrightDaemon.ensureExecutor", bsSession=sid)
70
+ except Exception as e:
71
+ raise ExecutorUnavailable(
72
+ f"ensureExecutor failed for session {sid!r}: {e}"
73
+ ) from e
74
+ sock_path = res.get("exec_sock") if isinstance(res, dict) else None
75
+ executor_id = res.get("executor_id") if isinstance(res, dict) else None
76
+ if not isinstance(sock_path, str) or not sock_path:
77
+ raise ExecutorUnavailable(
78
+ f"ensureExecutor returned no socket for session {sid!r}: {res!r}"
79
+ )
80
+ if not isinstance(executor_id, str) or not executor_id:
81
+ raise ExecutorUnavailable(
82
+ "ensureExecutor returned no executor instance identity; restart "
83
+ "the daemon so timeout cleanup cannot target a newer process"
84
+ )
85
+ return ExecutorLease(sock_path=sock_path, executor_id=executor_id)
86
+
87
+
88
+ def ensure_executor(sess) -> str:
89
+ """Compatibility wrapper returning only the executor socket path."""
90
+ return _ensure_executor_lease(sess).sock_path
91
+
92
+
93
+ def run_on_executor(
94
+ sess,
95
+ code: str,
96
+ *,
97
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
98
+ env: dict[str, str] | None = None,
99
+ ) -> ExecuteResponse:
100
+ """Ship ``code`` to the session's executor and return its response.
101
+
102
+ Ensures the executor (control plane), connects its socket (data plane),
103
+ sends one :class:`ExecuteRequest`, reads one :class:`ExecuteResponse`.
104
+
105
+ The executor enforces ``timeout_ms`` across cold-start and user code. A
106
+ terminal deadline/reset response is not returned to the caller until the
107
+ daemon confirms that exact executor instance has exited."""
108
+ return _run_request_on_executor(
109
+ sess,
110
+ ExecuteRequest(
111
+ code=code,
112
+ timeout_ms=timeout_ms,
113
+ env=dict(env or {}),
114
+ ),
115
+ )
116
+
117
+
118
+ def run_task_on_executor(
119
+ sess,
120
+ site: str,
121
+ name: str,
122
+ *,
123
+ args: dict | None = None,
124
+ isolated: bool = False,
125
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
126
+ env: dict[str, str] | None = None,
127
+ ) -> ExecuteResponse:
128
+ """Run one validated site-skill task on the resident executor surface."""
129
+ return _run_request_on_executor(
130
+ sess,
131
+ ExecuteRequest(
132
+ code="",
133
+ timeout_ms=timeout_ms,
134
+ env=dict(env or {}),
135
+ task=TaskEnvelope(
136
+ site=site,
137
+ name=name,
138
+ args=dict(args or {}),
139
+ isolated=isolated,
140
+ ),
141
+ ),
142
+ )
143
+
144
+
145
+ def _run_request_on_executor(
146
+ sess,
147
+ request: ExecuteRequest,
148
+ ) -> ExecuteResponse:
149
+ """Send one request through the shared lease/reap data-plane lifecycle."""
150
+ # Validate/copy before touching the daemon or opening a socket. The
151
+ # executor validates again at the trust boundary.
152
+ request = ExecuteRequest.from_dict(request.to_dict())
153
+ sid = _session_id(sess)
154
+ # Session idle is "time since the last user/agent instruction", not
155
+ # executor process liveness. Touch before contacting the executor so a
156
+ # wedged or long-running executor cannot prevent the durable idle clock
157
+ # from reflecting that a new instruction arrived.
158
+ reg.touch(sid)
159
+ lease = _ensure_executor_lease(sess)
160
+ # The executor owns the exact outer deadline, including first-call
161
+ # cold-start. Give only a small framing/delivery margin so its terminal
162
+ # response wins the race against the local socket timeout.
163
+ recv_timeout = (
164
+ max(request.timeout_ms, 1) / 1000.0 + _RESPONSE_DELIVERY_SLACK_S
165
+ )
166
+ conn = _connect(lease.sock_path, timeout=recv_timeout)
167
+ sent = False
168
+ interrupted: BaseException | None = None
169
+ transport_error: ConnectionError | OSError | ValueError | None = None
170
+ msg: dict | None = None
171
+ try:
172
+ with _sigterm_as_system_exit():
173
+ request.executor_id = lease.executor_id
174
+ send_message(conn, request.to_dict())
175
+ sent = True
176
+ msg = recv_message(conn)
177
+ except (KeyboardInterrupt, SystemExit) as e:
178
+ interrupted = e
179
+ except (ConnectionError, OSError, ValueError) as e:
180
+ transport_error = e
181
+ finally:
182
+ try:
183
+ conn.close()
184
+ except OSError:
185
+ pass
186
+
187
+ # Close the data plane before asking the daemon to wait for process death.
188
+ if interrupted is not None:
189
+ if sent:
190
+ _best_effort_recycle(sess, lease.executor_id)
191
+ raise interrupted
192
+ if transport_error is not None:
193
+ recycle_error: Exception | None = None
194
+ if sent:
195
+ try:
196
+ _confirm_recycled(sess, lease.executor_id)
197
+ except Exception as cleanup_error: # noqa: BLE001
198
+ recycle_error = cleanup_error
199
+ suffix = (
200
+ f"; executor reap could not be confirmed: {recycle_error}"
201
+ if recycle_error is not None
202
+ else ""
203
+ )
204
+ raise ExecutorUnavailable(
205
+ f"executor data-plane error on {lease.sock_path!r}: "
206
+ f"{transport_error}{suffix}"
207
+ ) from transport_error
208
+ if msg is None:
209
+ raise ExecutorUnavailable(
210
+ f"executor returned no response on {lease.sock_path!r}"
211
+ )
212
+ try:
213
+ response = ExecuteResponse.from_dict(msg)
214
+ except (TypeError, ValueError) as e:
215
+ try:
216
+ _confirm_recycled(sess, lease.executor_id)
217
+ except Exception as cleanup_error: # noqa: BLE001
218
+ raise ExecutorUnavailable(
219
+ "executor returned a malformed response and its reap could "
220
+ f"not be confirmed: {cleanup_error}"
221
+ ) from e
222
+ raise ExecutorUnavailable(
223
+ f"executor returned a malformed response: {e}"
224
+ ) from e
225
+ if response.terminal_reason is not None:
226
+ _confirm_recycled(sess, lease.executor_id)
227
+ return response
228
+
229
+
230
+ def _confirm_recycled(sess, executor_id: str) -> dict:
231
+ """Ask the daemon to reap one exact executor and wait for process death."""
232
+ sid = _session_id(sess)
233
+ result = sess.cdp.send(
234
+ "BrowserwrightDaemon.killExecutor",
235
+ bsSession=sid,
236
+ executorId=executor_id,
237
+ wait=True,
238
+ )
239
+ if not isinstance(result, dict) or result.get("reaped") is not True:
240
+ raise ExecutorUnavailable(
241
+ f"daemon did not confirm executor {executor_id!r} was reaped: {result!r}"
242
+ )
243
+ return result
244
+
245
+
246
+ def _best_effort_recycle(sess, executor_id: str) -> None:
247
+ try:
248
+ _confirm_recycled(sess, executor_id)
249
+ except BaseException: # noqa: BLE001 - cannot mask the original interrupt
250
+ return
251
+
252
+
253
+ @contextmanager
254
+ def _sigterm_as_system_exit() -> Iterator[None]:
255
+ """Make SIGTERM unwind the blocking recv so exact-instance reap is tried."""
256
+ if (
257
+ not hasattr(signal, "SIGTERM")
258
+ or threading.current_thread() is not threading.main_thread()
259
+ ):
260
+ yield
261
+ return
262
+ previous = signal.getsignal(signal.SIGTERM)
263
+
264
+ def _handle(signum, _frame):
265
+ raise SystemExit(128 + int(signum))
266
+
267
+ try:
268
+ signal.signal(signal.SIGTERM, _handle)
269
+ yield
270
+ finally:
271
+ signal.signal(signal.SIGTERM, previous)
272
+
273
+
274
+ def _connect(sock_path: str, *, timeout: float = 30.0,
275
+ retry_until: float = 5.0) -> socket.socket:
276
+ """Connect the executor's unix socket, briefly retrying a not-yet-bound
277
+ socket (the daemon returns the path the moment it spawns; the bind may race
278
+ by a few ms)."""
279
+ deadline = time.monotonic() + retry_until
280
+ last: OSError | None = None
281
+ while True:
282
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
283
+ s.settimeout(timeout)
284
+ try:
285
+ s.connect(sock_path)
286
+ return s
287
+ except OSError as e:
288
+ last = e
289
+ s.close()
290
+ if time.monotonic() >= deadline:
291
+ raise ExecutorUnavailable(
292
+ f"could not connect executor socket {sock_path!r}: {e}"
293
+ ) from last
294
+ time.sleep(0.05)
295
+
296
+
297
+ def _session_id(sess) -> str:
298
+ rec = getattr(sess, "session_record", None)
299
+ if isinstance(rec, dict) and rec.get("id"):
300
+ return str(rec["id"])
301
+ raise ExecutorUnavailable("no session id bound; cannot reach an executor")