effect-browserbase 0.1.0-beta.104 → 0.2.0-beta.0

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 (135) hide show
  1. package/README.md +61 -305
  2. package/dist/Account.d.mts +4 -6
  3. package/dist/Account.mjs +4 -9
  4. package/dist/Account.mjs.map +1 -1
  5. package/dist/{Acquisition-WBafHwXH.mjs → Acquisition-D8o4jtCg.mjs} +22 -68
  6. package/dist/Acquisition-D8o4jtCg.mjs.map +1 -0
  7. package/dist/Agents.d.mts +4 -6
  8. package/dist/Agents.mjs +3 -19
  9. package/dist/Agents.mjs.map +1 -1
  10. package/dist/Allocation.d.mts +9 -11
  11. package/dist/Allocation.mjs +2 -4
  12. package/dist/Allocation.mjs.map +1 -1
  13. package/dist/Browser.d.mts +23 -141
  14. package/dist/Browser.mjs +208 -164
  15. package/dist/Browser.mjs.map +1 -1
  16. package/dist/BrowserBinding.d.mts +7 -34
  17. package/dist/BrowserBinding.mjs +30 -1
  18. package/dist/BrowserBinding.mjs.map +1 -0
  19. package/dist/Certificates.d.mts +3 -5
  20. package/dist/Certificates.mjs +2 -7
  21. package/dist/Certificates.mjs.map +1 -1
  22. package/dist/{Cleanup-G2YpTCuU.d.mts → Cleanup-DMTDBir8.d.mts} +5 -6
  23. package/dist/Cleanup.d.mts +3 -2
  24. package/dist/Cleanup.mjs +1 -6
  25. package/dist/Cleanup.mjs.map +1 -1
  26. package/dist/{Client-D1_xYM9o.mjs → Client-BQteUePT.mjs} +2 -4
  27. package/dist/{Client-D1_xYM9o.mjs.map → Client-BQteUePT.mjs.map} +1 -1
  28. package/dist/Client.d.mts +51 -2
  29. package/dist/Client.mjs +1 -1
  30. package/dist/Connection-DcvJZEdc.mjs +65 -0
  31. package/dist/Connection-DcvJZEdc.mjs.map +1 -0
  32. package/dist/ContextCoordination.d.mts +5 -6
  33. package/dist/ContextCoordination.mjs +1 -8
  34. package/dist/ContextCoordination.mjs.map +1 -1
  35. package/dist/Contexts.d.mts +4 -6
  36. package/dist/Contexts.mjs +2 -8
  37. package/dist/Contexts.mjs.map +1 -1
  38. package/dist/Downloads.d.mts +6 -8
  39. package/dist/Downloads.mjs +3 -8
  40. package/dist/Downloads.mjs.map +1 -1
  41. package/dist/Errors.d.mts +3 -26
  42. package/dist/Errors.mjs +1 -137
  43. package/dist/Errors.mjs.map +1 -1
  44. package/dist/{Extensions-ClZbYb1H.mjs → Extensions-nMNMPGCc.mjs} +3 -9
  45. package/dist/{Extensions-ClZbYb1H.mjs.map → Extensions-nMNMPGCc.mjs.map} +1 -1
  46. package/dist/Extensions.d.mts +4 -6
  47. package/dist/Extensions.mjs +1 -1
  48. package/dist/Functions.d.mts +4 -6
  49. package/dist/Functions.mjs +3 -17
  50. package/dist/Functions.mjs.map +1 -1
  51. package/dist/{Launch-BHcAEq05.mjs → Launch-m0fxzJ49.mjs} +5 -5
  52. package/dist/Launch-m0fxzJ49.mjs.map +1 -0
  53. package/dist/Launch.d.mts +130 -2
  54. package/dist/Launch.mjs +4 -11
  55. package/dist/Launch.mjs.map +1 -1
  56. package/dist/PageFetch.d.mts +3 -5
  57. package/dist/PageFetch.mjs +2 -8
  58. package/dist/PageFetch.mjs.map +1 -1
  59. package/dist/Projects.d.mts +3 -5
  60. package/dist/Projects.mjs +2 -8
  61. package/dist/Projects.mjs.map +1 -1
  62. package/dist/Recordings.d.mts +6 -8
  63. package/dist/Recordings.mjs +3 -5
  64. package/dist/Recordings.mjs.map +1 -1
  65. package/dist/{References-URDAtapt.d.mts → References-DOH3Wu44.d.mts} +5 -6
  66. package/dist/References.d.mts +3 -2
  67. package/dist/References.mjs +10 -18
  68. package/dist/References.mjs.map +1 -1
  69. package/dist/Replays.d.mts +6 -8
  70. package/dist/Replays.mjs +3 -5
  71. package/dist/Replays.mjs.map +1 -1
  72. package/dist/Search.d.mts +3 -5
  73. package/dist/Search.mjs +2 -9
  74. package/dist/Search.mjs.map +1 -1
  75. package/dist/{SessionData-DRKcKCqw.d.mts → SessionData-5xYZDshJ.d.mts} +5 -6
  76. package/dist/SessionData.d.mts +3 -2
  77. package/dist/SessionData.mjs +1 -8
  78. package/dist/SessionData.mjs.map +1 -1
  79. package/dist/{Sessions-CtbseWEh.mjs → Sessions-DoQGg9nB.mjs} +34 -18
  80. package/dist/Sessions-DoQGg9nB.mjs.map +1 -0
  81. package/dist/{Sessions-B8E4kMxo.d.mts → Sessions-tFJYaXFq.d.mts} +8 -8
  82. package/dist/Sessions.d.mts +4 -3
  83. package/dist/Sessions.mjs +1 -1
  84. package/dist/{Transfers-Czv5IZzJ.d.mts → Transfers-GVqzrDWl.d.mts} +22 -9
  85. package/dist/Transfers.d.mts +3 -2
  86. package/dist/Transfers.mjs +12 -28
  87. package/dist/Transfers.mjs.map +1 -1
  88. package/dist/Uploads.d.mts +6 -8
  89. package/dist/Uploads.mjs +3 -5
  90. package/dist/Uploads.mjs.map +1 -1
  91. package/dist/Webhooks.d.mts +3 -5
  92. package/dist/Webhooks.mjs +2 -11
  93. package/dist/Webhooks.mjs.map +1 -1
  94. package/dist/{WriterFacts-osvYAsIx.d.mts → WriterFacts-5CPBGG1P.d.mts} +4 -3
  95. package/dist/index.d.mts +27 -31
  96. package/dist/index.mjs +26 -31
  97. package/package.json +3 -28
  98. package/dist/Acquisition-WBafHwXH.mjs.map +0 -1
  99. package/dist/Association-CEps4hYP.mjs +0 -10
  100. package/dist/Association-CEps4hYP.mjs.map +0 -1
  101. package/dist/Bootstrap-BXuzZojd.mjs +0 -194
  102. package/dist/Bootstrap-BXuzZojd.mjs.map +0 -1
  103. package/dist/Bootstrap-m7lHgj0D.d.mts +0 -177
  104. package/dist/Bootstrap.d.mts +0 -2
  105. package/dist/Bootstrap.mjs +0 -2
  106. package/dist/BrowserBinding-DlJTGYZL.mjs +0 -2508
  107. package/dist/BrowserBinding-DlJTGYZL.mjs.map +0 -1
  108. package/dist/BrowserData-LpcAuAub.d.mts +0 -369
  109. package/dist/BrowserData.d.mts +0 -2
  110. package/dist/BrowserData.mjs +0 -442
  111. package/dist/BrowserData.mjs.map +0 -1
  112. package/dist/Capture-Bc0z9IAi.mjs +0 -571
  113. package/dist/Capture-Bc0z9IAi.mjs.map +0 -1
  114. package/dist/Capture-BvWI-l1A.d.mts +0 -168
  115. package/dist/Capture.d.mts +0 -2
  116. package/dist/Capture.mjs +0 -2
  117. package/dist/Client-Ca0zSg1n.d.mts +0 -53
  118. package/dist/Images-DgscEfSf.mjs +0 -127
  119. package/dist/Images-DgscEfSf.mjs.map +0 -1
  120. package/dist/Launch-BHcAEq05.mjs.map +0 -1
  121. package/dist/Launch-C8CfaYxb.d.mts +0 -132
  122. package/dist/LocalBrowser-BLancJ3R.d.mts +0 -79
  123. package/dist/LocalBrowser-DsQLWNL1.mjs +0 -439
  124. package/dist/LocalBrowser-DsQLWNL1.mjs.map +0 -1
  125. package/dist/LocalBrowser.d.mts +0 -2
  126. package/dist/LocalBrowser.mjs +0 -2
  127. package/dist/PageControl.d.mts +0 -16
  128. package/dist/PageControl.mjs +0 -36
  129. package/dist/PageControl.mjs.map +0 -1
  130. package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
  131. package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
  132. package/dist/Session-D4nT9hOO.mjs +0 -1379
  133. package/dist/Session-D4nT9hOO.mjs.map +0 -1
  134. package/dist/Sessions-CtbseWEh.mjs.map +0 -1
  135. package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Browserbase for Effect
2
2
 
3
- `effect-browserbase` owns the whole Browserbase surface for an Effect application: account identity and transport, session, context and extension resources, one owned browser over Playwright/CDP — allocated or borrowed — with trusted registrations, modeled file selection, bounded live capture, explicit page holds, the provider's recordings, replays, uploads and website downloads, and the platform APIs outside a browser session (projects, certificates, Search, Fetch, Agents, Functions and webhooks).
3
+ `effect-browserbase` supplies Browserbase resources and hosted lifetimes to the shared browser runtime: account identity and transport, session, context and extension resources, one owned browser over Playwright/CDP — allocated or borrowed — with trusted registrations, modeled file selection, bounded live capture, explicit page holds, the provider's recordings, replays, uploads and website downloads, and the platform APIs outside a browser session (projects, certificates, Search, Fetch, Agents, Functions and webhooks).
4
4
 
5
- It has no Effect Agent dependency. Playwright is an optional peer, loaded lazily and only when a browser actually connects; a consumer that just reads artifacts never imports it. The package targets trusted Node and Bun hosts. It is an unpublished maintainer-review candidate: hosted provider behavior requires separate validation, and ordinary tests never create a paid Browserbase session or invoke a paid model.
5
+ Its host supplies the required `effect-browser` peer, at the same coordinated `0.2.0-beta.0` version for this candidate; it has no Effect Agent dependency. Install both packages explicitly so the provider and application share one runtime instance. Playwright is an optional peer of the shared runtime and is loaded only when a browser connects; artifact-only consumers do not need it. The existing Effect peer range remains `^4.0.0-rc.115`, with only rc.115 qualified by this candidate. The package targets trusted Node and Bun hosts. It is an unpublished maintainer-review candidate: hosted provider behavior requires separate validation, and ordinary tests never create a paid Browserbase session or invoke a paid model.
6
6
 
7
7
  ## One account, then resources
8
8
 
@@ -35,7 +35,8 @@ For the simplest post-session video path, opt in to Browserbase recording in the
35
35
 
36
36
  ```ts
37
37
  import { BrowserbaseBrowser } from "effect-browserbase/browser";
38
- import { BrowserPolicy, NavigateRequest } from "effect-browserbase/browser-data";
38
+ import * as Browser from "effect-browser/browser";
39
+ import { BrowserPolicy, NavigateRequest } from "effect-browser/browser-data";
39
40
  import { recipe } from "effect-browserbase/launch";
40
41
  import { BrowserbaseRecordings } from "effect-browserbase/recordings";
41
42
  import { RecordingPageReference } from "effect-browserbase/transfers";
@@ -52,16 +53,13 @@ const layers = Layer.merge(BrowserbaseBrowser.layer({ launch }), BrowserbaseReco
52
53
  );
53
54
 
54
55
  const program = Effect.gen(function* () {
55
- const browser = yield* BrowserbaseBrowser;
56
56
  const recordings = yield* BrowserbaseRecordings;
57
57
 
58
- const reference = yield* Effect.scoped(
58
+ const reference = yield* Browser.scoped(BrowserbaseBrowser.open(policy), (session) =>
59
59
  Effect.gen(function* () {
60
- const session = yield* browser.open(policy);
60
+ yield* session.navigate(NavigateRequest.make({ url: "https://example.com" }));
61
61
 
62
- yield* session.bind().navigate(NavigateRequest.make({ url: "https://example.com" }));
63
-
64
- // An AgentRuntime can borrow this same session through the adapter package.
62
+ // Browser.scoped checks the owner's release before returning this durable identity.
65
63
  return session.reference;
66
64
  }),
67
65
  );
@@ -91,7 +89,7 @@ The launch recipe deliberately differs from three Browserbase defaults. Set a fi
91
89
  | `logSession` | on | off | CDP logs carry page content and typed input |
92
90
  | `solveCaptchas` | on | off | solving is an action the host should choose |
93
91
 
94
- For model-driven control, use `effect-agent-browserbase`, whose tools borrow an already-owned session rather than allocating a browser per tool call. Provider credentials, CDP URLs, context choices, Live View controls and recording configuration are host decisions and are never Tool parameters.
92
+ For model-driven control, use `effect-agent-browser`, whose tools borrow an already-owned session rather than allocating a browser per tool call. Provider credentials, CDP URLs, context choices, Live View controls and recording configuration are host decisions and are never Tool parameters.
95
93
 
96
94
  ## Choose a video path
97
95
 
@@ -120,204 +118,30 @@ That split is why an agent can use one browser for many turns while provider art
120
118
 
121
119
  `Allocation.scoped(recipe)` allocates a session without connecting a browser, for a caller that drives the remote session with its own automation client. It is the same allocation path the browser owner uses, so the creation request is never retried, an unknown outcome is still reported exactly once, a persisting Context still needs a live writer permit, and no native peer is loaded. The scope owns the release: closing it releases the session whether or not anything ever connected. A reply naming another project is refused, and because a foreign reference is never acted on, the caller is handed that reference rather than a mutation attempted against it.
122
120
 
123
- The browser owner does not allocate or release anything itself. It supplies the local half of cleanup — fence, capture stop, initialization teardown, disconnect — and the canonical control plane owns the release request and the terminal status observation. A local disconnect, an accepted provider release request and provider-confirmed termination therefore stay distinct facts in one `CleanupResult`, together with the exact steps that failed.
124
-
125
- Mutations are serialized. An observation identifies retained native nodes only until the next invalidating event; it is not a DOM snapshot version. Replaced or detached nodes fail instead of silently resolving to replacements. An action interrupted or timed out after native dispatch has an **unknown** outcome: the owner is fenced and the package never automatically replays it. `undispatched` is used only when the package established that native mutation dispatch did not occur.
126
-
127
- ### A navigation you can watch while it loads
128
-
129
- `navigate` holds nothing open that you can see into: it returns when the document reaches DOMContentLoaded. `startNavigation` is the same single dispatch, left in flight, so a recorder can look at a page while it is still arriving, hold it, and let it finish:
130
-
131
- ```ts
132
- const operation =
133
- yield * handle.startNavigation(StartNavigationRequest.make({ url, timeoutMillis: 30_000 }));
134
-
135
- const early = yield * session.checkpoint({ picture: true }); // what has loaded so far
136
- const receipt = yield * PageControl.suspend(session, page); // timers, CSS and parsing stop
137
- yield * PageControl.resume(session, receipt);
138
- const { url: loaded } = yield * operation.completed;
139
- ```
140
-
141
- The owner's permit is released as soon as the navigation is dispatched. While it loads, reads, checkpoints, holds and every other page proceed, and anything that would change _this_ page fails `busy` and `undispatched`. `navigate` runs on the same machinery, so there is one navigator.
142
-
143
- - `completed` belongs to that one navigation: a successor reaching the same URL fails it instead of completing it, and its URL is the page that navigated, not whichever page is selected by then. **Interrupting a waiter stops nothing.** The browser keeps loading and nothing is dispatched again.
144
- - `stop` asks the browser to stop loading. Its acknowledgement is a known outcome: `completed` then fails `interrupted`, the page holds whatever had loaded, and the session stays usable. It does not undo anything the page already did.
145
- - Leaving the operation's scope unsettled, or a navigation that fails after dispatch, fences the session as uncertain, exactly as an interrupted mutation always has. Nothing knows what the browser did, so nothing more is sent, and it is never replayed.
146
-
147
- A read is ordered against the document being replaced by failing: if the document it was reading was replaced underneath it, or a navigation is still in flight on its page, the error is `target-changed` and `undispatched`, which means read again. A read is never a mutation, so that is always safe. A held page is not read at all; take the checkpoint before the hold and keep it.
148
-
149
- Locally, a hold during an incremental response stops parsing as well as timers: a chunk the server sends meanwhile is not parsed until resume. That is an observation of the pinned Chromium, not a guarantee about hosted sessions.
150
-
151
- ### What is on screen, and what a host may know about it
152
-
153
- `session.observe()` reads the whole document. `session.observe({ scope: "viewport" })` keeps only text and controls that are on screen and reachable, and says what it left out:
154
-
155
- ```ts
156
- const seen = yield * session.observe({ scope: "viewport" });
157
- seen.viewport; // { clippedText, coveredText, uncertainText, unreachableControls, exhausted, ... }
158
- ```
159
-
160
- Visibility here is geometry and hit-testing, never a pixel comparison, and the counts say which was which. Text is kept when its line boxes intersect the viewport and the browser finds its own element at a sampled point. A text node that crosses the viewport edge contributes only its lines on screen (`clippedText`). Text behind another element is left out (`coveredText`). Text under something that takes no pointer events cannot be hit-tested at all, so it is left out as `uncertainText` rather than called visible. `exhausted` means the traversal budget ran out first and the reading is known to be incomplete. Canvas pixels and compositing effects are not interpreted.
161
-
162
- An `Observation` is safe to show a model, and the adapter's `browser_inspect` Tool returns it as is. It therefore carries no destination, form target or field value, in either scope. What a host needs to decide whether a control may be acted on is a separate, host-only read from the exact node:
163
-
164
- ```ts
165
- const facts = yield * session.controlFacts(reference);
166
- // kind, label, disabled, editable, inputType, autocomplete, formMethod, box, placement, hitTest
167
- // destination: the resolved link target, or where this control submits its form
168
- ```
169
-
170
- `destination` is resolved by the browser against the document's base URL, and honours a `formaction` override. It can carry a token, which is why it is not in an `Observation`. An over-long destination is left out, never cut. No value and no markup is ever included.
171
-
172
- A reference is to the control that was inspected, not merely to a node. If the same attached node now has a different destination, input type, autocomplete category, form method, label or disabled state, acting on it fails `stale` and `undispatched`, exactly as it does for a replaced or detached node. Nothing is ever re-found by selector or label.
173
-
174
- When a decision has to be current at dispatch, pass a policy. It is evaluated on facts read from that exact node immediately before the input:
175
-
176
- ```ts
177
- yield *
178
- session.fillElement(reference, value, {
179
- admit: (facts) => facts.inputType !== "password" && facts.autocomplete !== "cc-number",
180
- });
181
- ```
182
-
183
- Anything but `true`, or a policy that throws, sends nothing and fails `denied`. The policy is a plain synchronous function on purpose: it runs while the owner's permit is held, where waiting on a model or a network call would stall every other operation. It is not an atomic check-and-input transaction, because page script can still run before the native input lands.
184
-
185
- ### Passive checkpoints for a recorder
186
-
187
- `observe()` replaces the one observation whose nodes later actions may name, so a recorder calling it would retire the references an agent is about to use. `session.checkpoint()` is the passive path: viewport text, control facts and, when asked, a PNG of the viewport.
188
-
189
- ```ts
190
- const checkpoint = yield * session.checkpoint({ picture: true });
191
- ```
192
-
193
- It issues no references, is not a mutation, and leaves the action observation and the selection exactly as they were, so inspect, checkpoint, then act on the inspected node all compose. It is host-only, because it carries control facts. Text and picture are read one after the other, never atomically: the interval is on the host monotonic clock, and `documentChanged` says the document was replaced in between. A checkpoint is charged as one action, like any other bounded read. A held page is refused `busy` rather than woken to be read: take the checkpoint before the hold and keep it.
194
-
195
- ### Real pointer and wheel input
196
-
197
- `pointerMove`, `hover` and `wheel` send the input a person's hardware would, so pages see trusted events, `:hover` applies, and the browser itself decides what is under the pointer. `scroll` stays what it was: script in the page, instantaneous, raising no wheel event. That difference is how a recording tells one from the other.
198
-
199
- ```ts
200
- const handle = session.bind();
201
-
202
- yield * handle.pointerMove(PointerMoveRequest.make({ to: { x: 140, y: 100 } }));
203
- yield * handle.hover(HoverRequest.make({ selector: "#menu" }));
204
- // A nested scroll container under the pointer scrolls, not the page.
205
- const receipt =
206
- yield * handle.wheel(WheelRequest.make({ deltaX: 0, deltaY: 240, at: { x: 420, y: 120 } }));
207
- ```
208
-
209
- Coordinates are CSS pixels in the main frame's viewport. Each call is one native command, charged as one action and fenced like any other mutation, so a handle bound to a page that is no longer selected sends nothing to either page. Easing, pacing and cursor artwork are yours: send the points you want, and draw the cursor from the positions the receipts report.
210
-
211
- `hover` places the pointer on one exact element where it is, by selector or by the node an observation named (`session.hoverElement`). It never scrolls to reach it, because that would hide a scripted scroll inside a native-input operation. If the pointer cannot be placed on the element (it is outside the viewport, has no area, or something covers it) the call fails `not-visible` and `undispatched`.
212
-
213
- For a child-frame element, hover checks the commanded point through each ancestor
214
- frame and then the exact node, including cross-origin documents. The receipt
215
- still uses main-viewport CSS coordinates. An overlay or clipping ancestor refuses
216
- input. Frame traversal is bounded at 32 levels, with bounded shadow/DOM ancestry
217
- checks. Axis-aligned translation and positive scaling are supported; rotation,
218
- perspective and other unsupported frame mappings are refused rather than guessed.
219
- These reads and native input are separate operations in the browser: page script
220
- can still change geometry between validation and dispatch.
221
-
222
- An `InputReceipt` carries the target it was sent to, the position this owner commanded (null until it has placed the pointer on that page), and an interval on the same host monotonic clock that stamps `CapturedFrame.receivedMonotonicNanos`. Input and pixels share one timeline, so a compositor can place the pointer on the frame that shows it. A wheel event is dispatched, not awaited: the receipt does not claim the page finished scrolling or that any frame shows it.
223
-
224
- ### Real key input
225
-
226
- `fill` sets a field's value in one step: the page gets an `input` event and no `keydown`, `keypress` or `keyup`, so anything that reacts to keys behaves differently under a recorder than it does for a person. `press` and `type` send the strokes a keyboard would. Handlers see trusted key events, and the browser does what it does for a person: Tab moves focus and selects the field it lands in, Enter submits a form that has a submit button, and Backspace edits.
227
-
228
- ```ts
229
- const handle = session.bind();
230
-
231
- // Focus is the page's business. A real click gives it, and keys then follow it.
232
- yield * handle.click(ClickRequest.make({ selector: "#from" }));
233
- yield * handle.type(TypeRequest.make({ text: "Vienna" }));
234
- yield * handle.press(PressRequest.make({ key: "Backspace" }));
235
- yield * handle.press(PressRequest.make({ key: "k", modifiers: ["Control"] }));
236
- // Sent only if `#from` still has focus; otherwise nothing is sent at all.
237
- yield * handle.press(PressRequest.make({ key: "Enter", into: "#from" }));
238
- ```
239
-
240
- Keys go to whatever has focus in the selected page, in whichever frame that is, because that is where the browser sends them. `into` narrows that to one exact element, which must already have focus or hold the element that does, through any open shadow root. If it does not, the call fails `not-focused` and `undispatched`. It never focuses the element for you, for the reason `hover` never scrolls: that would hide a scripted focus inside a native-input operation. `session.pressElement` and `session.typeElement` apply the same rule to the node an observation named and take the same host admission as `fillElement`, so switching from `fill` to real typing gives up neither exactness nor the check on fresh control facts.
121
+ The shared browser runtime delegates allocation and release to this provider lifetime. It supplies the local half of cleanup — fence, capture stop, initialization teardown, disconnect — and the canonical control plane owns the release request and the terminal status observation. A local disconnect, an accepted provider release request and provider-confirmed termination therefore stay distinct facts in one `CleanupResult`, together with the exact steps that failed.
241
122
 
242
- A key is spelled as the `KeyboardEvent.key` the page will see, and the vocabulary is closed: `Enter`, `Tab`, `Backspace`, `Delete`, `Escape`, the four arrows, `Home`, `End`, `PageUp`, `PageDown`, or one printable ASCII character (a space is `" "`), with `Shift`, `Control`, `Alt` and `Meta` as modifiers. The native engine parses a key string, chords included, and begins holding the modifiers before it has validated the key, so nothing reaches it that was not reviewed here. A modifier other than Shift makes a chord rather than a character, and nothing is typed.
123
+ `onCleanup` runs once after the canonical receipt and any Context-writer settlement are recorded.
124
+ Its construction throws, defects, self-interruption and two-second cooperative timeout are
125
+ contained separately from the built-in diagnostics reporter. A failed reporter cannot suppress
126
+ the sink, and a failed sink cannot change cleanup facts or repeat release. The receipt remains
127
+ readable through `cleanupResult`; `closeChecked` still fails when its actual confirmation predicate
128
+ fails. An outer race can discard that checked error, so keep any required host receipt sink outside
129
+ the raced workflow. The shared guide's [Layer and receipt examples](../browser/README.md#a-long-lived-session-in-a-layer)
130
+ describe sharing one bounded session and the supervision that a Layer alone does not provide.
243
131
 
244
- `type` sends up to 256 characters as one charged action, two native commands for each, one after another under a single action timeout. On a slow link a long passage can outlast that timeout, which leaves an unknown outcome and fences the session like any interrupted mutation, so send it as several shorter runs. The owner is checked between characters, which means a fence stops the rest instead of typing into a session that is closing. Two limits come from the pinned engine. A character the US layout cannot produce is committed as text, the way an input method commits it: the field changes and no key event says so. And a shifted character arrives as its own key with `shiftKey` false. When a page reads the modifier, send that stroke through `press` with `Shift` held, spelling the key as the page will see it: `{ key: "A", modifiers: ["Shift"] }`. Spelled `"a"`, the engine sends `a` with Shift down, which is what Shift produces with Caps Lock on. Control characters are refused in text because the engine presses Enter for a line break; a named key is always its own `press`. Pacing is yours, as easing is for the pointer: for a typist's cadence, send one character per call and sleep between them, at one action each.
132
+ Mutations are serialized. An observation identifies retained native nodes only until the next invalidating event; it is not a DOM snapshot version. Replaced or detached nodes fail instead of silently resolving to replacements. An action interrupted or timed out after native dispatch has an **unknown** outcome and is never automatically replayed. Unresolved control fences the owner; the shared runtime's main-frame navigation recovery can preserve usability after one acknowledged stop while still reporting `Timeout/unknown`. Recovery is bounded by the loading deadline plus at most three seconds and the existing lifetime. Child-frame timeouts retain the conservative fence. `undispatched` is used only when the package established that native mutation dispatch did not occur.
245
133
 
246
- A press is dispatched, not awaited. If Enter submits a form, wait for what the next document shows with `waitFor`. A receipt carries the same target, pointer position and interval as any other input, and never says which key was pressed or what was typed. Typing a secret is still more observable than one `fill`, because the page sees every stroke; prefer `fill` for one unless the page requires keys. Neither operation is part of the model-facing toolkit in `effect-agent-browserbase`.
134
+ ## Shared browser operations
247
135
 
248
- The connection endpoint is read through the exact allocated session, so a provider reply that names a different session is refused before any CDP attachment.
136
+ Navigation, observation, native input, typed bootstrap bindings, live capture and page holds are implemented by [`effect-browser`](../browser/README.md). Import their contracts from that package and use them with the exact `BrowserbaseSession` returned here. The [`effect-agent-browser`](../agent-browser/README.md) adapter and maintained tools accept both this session and self-managed Chromium without a second connection.
249
137
 
250
- Persistent Browserbase contexts require a live writer permit from `ContextCoordination.withWriter` when writes are persisted. Detach/reconnect is opt-in with `keepAlive`; reconnect creates a new handle generation, verifies the selected target, obtains fresh state, and never replays pending input or treats serialized agent state as a live browser.
251
-
252
- Human handoff pauses automation before returning host-only Live View material. Resume requires an explicit operator-release signal and obtains a fresh observation while holding the same mutation permit. A failed handoff does not silently resume automation. Live View URLs are temporary bearer material; iframe styling is not an authorization boundary. Live View is also where browser-window presentation already exists for watching a session as it runs, and it is the provider's: beside each full-screen URL Browserbase issues a bordered one (`debuggerUrl`, "mimic a real browser with borders"), and a navbar that `navbar=false` hides. This package decodes and returns only the full-screen URL. The bordered one carries the same control authority and would be issued under the same rules, so surfacing it is a small host-only addition whenever something needs it; nothing here does yet, so it is not exported.
253
-
254
- ## Registrations, capabilities and document readiness
255
-
256
- `browser.open(policy, { bootstrap })`, `browser.acquire(policy, { bootstrap })` and `browser.attach(reference, { policy, bootstrap })` take a trusted registration plan at acquisition. The browser Layer fixes account and launch configuration, not consumer callback dependencies. A plan is built from `Bootstrap.binding`, `Bootstrap.init`, `Bootstrap.permissions` and `Bootstrap.combine`. Combination preserves the error and service unions of different handlers; the callable bridge precedes all dependent init steps in one native script registration. Script content, handler implementations and origin grants are host configuration, never model output or page input.
257
-
258
- ```ts
259
- import * as Bootstrap from "effect-browserbase/bootstrap";
260
- import { BrowserbaseBrowser } from "effect-browserbase/browser";
261
- import { BrowserPolicy } from "effect-browserbase/browser-data";
262
- import { Context, Effect, Schema } from "effect";
263
-
264
- class ShowSettings extends Context.Service<ShowSettings, { readonly title: string }>()(
265
- "ShowSettings",
266
- ) {}
267
-
268
- const bootstrap = Bootstrap.combine(
269
- Bootstrap.permissions({ origin: "https://portal.example.com", permissions: ["clipboard-read"] }),
270
- Bootstrap.binding({
271
- name: "getShowSettings",
272
- origins: ["https://portal.example.com"],
273
- input: Schema.Struct({ version: Schema.Literal(3) }),
274
- output: Schema.Struct({ title: Schema.String }),
275
- maxConcurrent: 2,
276
- maxInputBytes: 256,
277
- maxOutputBytes: 4096,
278
- timeoutMillis: 2000,
279
- failureMode: "fail-session",
280
- handle: () => Effect.map(ShowSettings, (settings) => ({ title: settings.title })),
281
- }),
282
- Bootstrap.init({
283
- id: "show-settings-v3",
284
- origins: ["https://portal.example.com"],
285
- content: "globalThis.__ready = globalThis.getShowSettings({ version: 3 }).then(() => true);",
286
- readiness: {
287
- expression: "globalThis.__ready",
288
- timeoutMillis: 5_000,
289
- existingDocuments: "RequireFreshNavigation",
290
- },
291
- }),
292
- );
293
-
294
- const run = Effect.gen(function* () {
295
- const browser = yield* BrowserbaseBrowser;
296
- return yield* browser.withBrowser(BrowserPolicy.unrestricted(), { bootstrap }, (session) =>
297
- Effect.gen(function* () {
298
- yield* session.bind().navigate({ url: "https://portal.example.com" });
299
- yield* session.ready;
300
- return yield* session.observe();
301
- }),
302
- );
303
- });
304
- // run requires BrowserbaseBrowser and ShowSettings. withBrowser discharges both the
305
- // callback's and the use function's Scope, but neither one's other services or errors.
306
- ```
307
-
308
- Registration is installed before this connection creates any document, and permissions precede the bundle. It is still not readiness: an asynchronous step cannot pause a website's own scripts, so a document is ready only when its expression resolves to exactly `true`. Readiness is keyed by frame and document epoch and evaluated once per document, so a completed wait can never ready the document that replaced the one it observed.
309
-
310
- Each binding accepts exactly one JSON-compatible argument and returns the output codec's **encoded** JSON value. A transforming codec such as `Schema.FiniteFromString` therefore exposes a string to the page while its host handler works with a number. The native callback validates the actual caller's allowed origin and document before input decoding, immediately before invoking the handler, and before replying. This uses Chromium's execution-context identity and `uniqueContextId` on child CDP sessions belonging to the existing connection: a frame URL, a page-supplied origin, and a reused numeric context id are not authorization. The pinned Playwright `exposeBinding` callback supplies a frame but not the calling document's identity; it is deliberately not used as a weaker substitute. No raw protocol or second browser owner is exposed.
311
-
312
- Plans admit at most 16 uniquely named bindings. Each binding declares its concurrent-call, UTF-8 input/output byte and whole-invocation deadline limits. Admission reserves capacity before native validation, codec work or a callback fiber starts; a timed-out native operation retains that reservation until it actually settles, including across reconnect. The page wrapper additionally rejects cyclic, sparse, accessor-bearing, non-plain, non-finite and non-JSON input rather than silently changing it through `JSON.stringify`. Its traversal admits at most 64 levels and 65,536 nodes; the configured byte limit still applies. Native target and default-document registries are finite, and closed native targets retire their authority immediately.
313
-
314
- `reject-call` rejects only the affected invocation and permits subsequent healthy calls. `fail-session` completes `session.failure` with the original typed consumer cause and fences the owner. Pages receive only `BrowserbaseBindingError: Browser binding call rejected`, with no host stack, consumer error payload, credentials or SDK cause. `session.bindingDiagnostics` is a bounded **host-only** snapshot containing per-binding accounting and the latest 32 typed causes; do not serialize it into a Tool response. Callback service reads run independently of a browser mutation, but reentrant browser work never waits behind that mutation's permit: it fails `busy` with `undispatched` instead.
315
-
316
- `withBrowser` races the use function against fail-session errors, discharges the scope, and permits normal success only after confirmed owned-session termination and complete local cleanup. Explicit `acquire`/`open` retain the typed failure signal and detailed cleanup receipt for callers that need to manage that decision themselves. Teardown synchronously closes callback admission, interrupts managed callback fibers, removes this connection's registrations, disconnects locally, and still attempts remote release if a prior cleanup step fails. Reconnect installs fresh callable registrations, never replays an old invocation or a consumer init script into an already-running document, and cannot reuse quarantined callback capacity.
317
-
318
- Operations that depend on an initialized document wait for the current one. Navigation, selection and page management do not, so initialization cannot deadlock the navigation that produces the document it is waiting for. A document that was already running when the bundle was registered — the page you attach to, or the one a reconnect finds — never ran it: `RequireFreshNavigation` reports `RequiresNavigation` and refuses dependent work, while `AcceptAlreadyRunning` verifies the requirement against that document instead of assuming it. Neither reloads a page whose work may be uncertain; that stays your decision. `session.ready` reports the current document without charging an action, and an origin outside the plan is reported as `NotApplicable` rather than waited on.
319
-
320
- The reviewed permission subset is granted against a real browser in native acceptance rather than copied from a list. Extension identity and storage across Context reuse, and registration retention across provider reconnects, remain hosted questions and are not claimed here.
138
+ The shared session also provides passive `status` and bounded `diagnostics`, including after
139
+ closure. Known expiry, callback failure and policy pressure are not automatically labeled
140
+ uncertain; unresolved native dispatch remains a separate fact. Owned provider termination can
141
+ retire native control, whereas borrowed disconnection alone cannot. These snapshots do not replace
142
+ the provider's canonical cleanup receipt or make business outcomes known. `maxHostReads` is a
143
+ browser Layer option (default 10,000, maximum 1,000,000) for checkpoint/control-facts sampling;
144
+ it is independent of model action accounting and never a tool parameter.
321
145
 
322
146
  ## Files in and out
323
147
 
@@ -329,13 +153,41 @@ Larger files use `BrowserbaseUploads.create`, which places bytes for the exact r
329
153
 
330
154
  `BrowserbaseBrowser.attach(reference, { policy, target })` takes control of a session this process did not allocate. It reads status first, so project authority is checked before anything is borrowed; a terminal session is refused because that needs a fresh allocation rather than a reattachment, and a starting session is admitted only within a bounded `pendingWaitMillis`. The requested target is resolved explicitly — there is no positional first-tab fallback — and connection credentials are fetched fresh, because expiry races the caller's own status read.
331
155
 
332
- A borrowed scope disconnects locally and reports `ownership: "borrowed"` with `remote: "not-owned"`. It never requests release and never claims Context-writer authority: whoever allocated the session keeps both. It also does not detach and reattach inside itself; attaching again is the cross-process path, and it revalidates the session instead of assuming it is still there. A prior uncertain business mutation is still yours to reconcile before the next one; nothing is replayed automatically.
156
+ `session.closeChecked` accepts confirmed owned cleanup or complete borrowed disconnection according to the actual ownership, then returns the same frozen `CleanupResult` cached by `close`. The concrete success value replaces the former `undefined`; failed confirmation remains a typed `BrowserError`. A borrowed scope disconnects locally and reports `ownership: "borrowed"` with `remote: "not-owned"`. It never requests release and never claims Context-writer authority: whoever allocated the session keeps both. It also does not detach and reattach inside itself; attaching again is the cross-process path, and it revalidates the session instead of assuming it is still there. A prior uncertain business mutation is still yours to reconcile before the next one; nothing is replayed automatically.
157
+
158
+ ## Shared API migration
159
+
160
+ | Before | Now |
161
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
162
+ | `session.bind()` or `yield* session.currentTarget` | Ordinary calls use `session` directly; `yield* session.retain` explicitly obtains checked stale-on-selection semantics |
163
+ | Bare page IDs in selection/closure, selection returns a handle | Pass `PageInfo` to `selectPage`/`closePage`; selection returns `void`, and `createPage` returns the exact created record |
164
+ | Reuse old page/frame metadata after reconnect | In the same known browser lifetime, read fresh pages and match exactly one saved `targetId`; then read fresh frames. No fallback to order, old ID, URL or title |
165
+ | `Adapter.fromSession(session)` or `currentHandle` | Await the Effect with explicit `{ selection: "current" }` or `{ selection: "retained" }`; its browser is the original concrete session |
166
+ | `BrowserError.reason` string and optional outcome | Tagged reason and required outcome. Use `Effect.catchReason`; factual limit/status/retry fields belong to their reason |
167
+ | All host reason names reach model output | A compact eleven-reason tool failure; original structured fields and bounded call IDs remain in `ToolHost.toolFailures` |
168
+ | `closeChecked` success is `undefined` | Concrete receipt success after the existing ownership check; `close` and `cleanupResult` remain available |
169
+ | Capture `dropped` | `discarded = overflow + late + duplicates + rejected`; no change to default capacity or unknown upstream loss |
170
+ | Non-supervising helper takes default `BrowserSession` | Use `AnySession`; keep supervising helpers generic in callback error or concrete session |
171
+
172
+ Binding omission defaults are one concurrent call, 64 KiB each direction, 10 seconds and
173
+ `reject-call`; explicit values remain validated. Combined readiness uses the most conservative
174
+ `existingDocuments` policy. Common navigation now accepts optional `timeoutMillis` with the same
175
+ bounds as `startNavigation`; direct, retained and pinned operations share it. Observations add
176
+ bounded checked/selected/inputType/required state without field values or destinations. These
177
+ contracts live in the [common browser guide](../browser/README.md).
178
+
179
+ Explicit generic applications of curried `Browser.scoped` use four outer parameters and three
180
+ returned parameters; ordinary inferred syntax is unchanged. Provider resource errors such as
181
+ `SessionError` keep their independent reason schemas. When a control-plane read becomes a browser
182
+ operation failure, the bridge preserves available facts; a resource limit with no measured bound
183
+ becomes `Provider` rather than fabricating `Limit` values. A native connection whose setup fails
184
+ after connection work began carries `unknown` outcome.
333
185
 
334
186
  `test/native/handoff-process.test.ts` exercises that path with a real second process: it starts a fresh runtime that receives only the reference, a target id and fixture addresses, attaches, changes the page and closes as a borrower, and the allocating process then drives the same page and releases it. That is local CDP evidence, not a hosted handoff.
335
187
 
336
188
  ## The native engine
337
189
 
338
- `browser-binding` names the engine an owned or borrowed browser connects through. The default is Playwright over CDP with the provider's own address, so nothing has to be provided; `BrowserBinding.layer(binding)` supplies another to every browser Layer built beneath it. A binding is opaque and only this module issues one, so a value that merely has its shape is refused before anything is allocated.
190
+ `browser-binding` names the engine an owned or borrowed browser connects through. The default is Playwright over CDP with the provider's own address, so nothing has to be provided; `BrowserBinding.layer(binding)` supplies another to every browser Layer built beneath it. A binding is opaque and issued by the common runtime, so a value that merely has its shape is refused before anything is allocated.
339
191
 
340
192
  `BrowserBinding.playwright({ resolveEndpoint, onConnected })` is trusted host configuration for running the unmodified Playwright engine somewhere else, such as a local Chromium. `resolveEndpoint` runs only after the provider-issued address passed the default checks, and chooses the CDP endpoint actually connected to; `onConnected` sees the engine's own browser object before the owner drives it. Neither is model-facing, and a local engine reached this way says nothing about how a hosted connection behaves. This package's own native fixtures use exactly this, rather than replacing `chromium.connectOverCDP` globally.
341
193
 
@@ -391,12 +243,9 @@ An agent run or Function invocation that persists a Context writes it from Brows
391
243
  - `sessions`, `contexts`, `context-coordination` — passive inspection, explicit release, context resources and writer settlement.
392
244
  - `extensions` — provisioning a Chrome extension archive once as a durable project resource, and selecting it by reference at launch.
393
245
  - `uploads` — placing a file where the running session can already reach it, and the receipt that authorizes attaching it.
394
- - `bootstrap` — E/R-preserving bounded typed bindings, one ordered init bundle, reviewed permission grants, per-document readiness and bounded host-only callback diagnostics.
395
- - `launch`, `references`, `browser-data`, `session-data`, `cleanup`, `transfers`, `errors` — credential-free schemas and typed expected errors.
246
+ - `launch`, `references`, `session-data`, `cleanup`, `transfers`, `errors` — credential-free schemas and typed expected errors.
396
247
  - `browser-binding` — the trusted, opaque native engine a browser connects through: Playwright by default, or Playwright routed to a host-resolved endpoint.
397
248
  - `browser` — scoped allocation, borrowed attachment to a running session, deterministic page control, real pointer, wheel and key input, host-only tabs/frames/viewport, modeled file selection, Live View handoff, keep-alive detach and explicit reconnect.
398
- - `capture` — optional target-pinned live-page JPEG frame streams using Playwright 1.63's maintained screencast API. The caller owns encoding, storage and presentation.
399
- - `page-control` — opt-in host-owned stage holds and explicit receipt-based resume, independent of scout selection.
400
249
  - `recordings` — post-session Browserbase MP4 assembly, status and bounded retrieval. Stable identity is session + recording page; signed URLs are refreshed and are not durable identity.
401
250
  - `replays` — host-authorized replay metadata and validated HLS media proxy material. It is playback access to a recording, not another recording.
402
251
  - `downloads` — ordinary website download metadata, provider-side filters, bounded byte streams and deletion, separate from provider recordings.
@@ -405,7 +254,7 @@ An agent run or Function invocation that persists a Context writes it from Brows
405
254
 
406
255
  Every expected failure says three things. `operation` is what you asked for, from a closed vocabulary per error class: `BrowserError` names the browser operations, `SessionError` only session calls, and so on, so you can match on them exhaustively and a misspelling is a type error, not a string that happens to compile. `reason` is why it failed. `outcome`, when present, is whether the work was sent: `undispatched` is safe to retry, `rejected` was refused, and `unknown` means a mutation may have happened and is never replayed for you. Why an extension archive was refused is a `reason` (`limit`, `unsafe-filename`, `configuration`) of the one `extension-archive` operation. A native step's own name never appears: the driver raises a private failure, and the owner stamps the operation it admitted.
407
256
 
408
- Everything under `src/internal/` is private, and no consumer CDP seam or lower-level lifecycle Layer is exported; `browser-binding` chooses the engine and where it connects, never what runs over the connection. The driver does hold a CDP session; exposing it, or the ownership internals, would place actions outside the mutation permit that serializes them and outside the fencing that makes an uncertain outcome detectable. Opening a second debugger connection beside this one has the same effect and is equally unsupported. An unmodeled need is a request for a modeled entry point, not a reason to reach around the boundary.
257
+ Everything under `src/internal/` is private; the common runtime offers a modeled integration constructor, while no consumer CDP seam is exported; `browser-binding` chooses the engine and where it connects, never what runs over the connection. The driver does hold a CDP session; exposing it, or the ownership internals, would place actions outside the mutation permit that serializes them and outside the fencing that makes an uncertain outcome detectable. Opening a second debugger connection beside this one has the same effect and is equally unsupported. An unmodeled need is a request for a modeled entry point, not a reason to reach around the boundary.
409
258
 
410
259
  ## Artifact and capture guarantees
411
260
 
@@ -415,101 +264,8 @@ Website downloads retain their provider download ID, safe filename, MIME type an
415
264
 
416
265
  Replay playlists reject arbitrary URI-bearing tags and proxy only indexed media from the validated playlist. API credentials are never returned to a browser client.
417
266
 
418
- Live capture frames carry owned JPEG bytes, captured target identity, sequence number, source presentation time, host monotonic receipt time, geometry and explicit drop accounting. Buffers are bounded by frame count and bytes; slow consumers drop old frames instead of creating an unbounded fiber/callback backlog. Buffer dropping is not page-clock backpressure and does not reduce what the browser produced upstream. Holding a capture callback is not a promise that page timers or animations stop.
419
-
420
- `sourceTimeMillis` is the browser's wall clock when it took the frame for the screencast, stamped before the frame is encoded. Chromium encodes up to three frames at once and emits each when its encode completes, so two frames stamped close together can arrive in either order. A frame that arrives behind a newer one can no longer be presented in order: it is discarded and counted in `late`, which is part of `dropped`. It is never sorted back in or given another time, so delivered source times strictly increase and a gap in `sequence` marks the omission. Concurrent encoding can put at most two late frames in a row. A longer run means source time itself went backwards, and the interval ends with reason `timestamp`.
421
-
422
- Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(session, { lifetime: "page" })` instead follows a page's main frame across documents: start it before a navigation and it covers the loading in between. The native screencast is never restarted for a navigation, so a boundary is not a gap this package introduced. Each frame carries the `document` it was received during (0, then one more per navigation), and the summary's bounded `documentBoundaries` give the last sequence before each one and the address it committed, with `initialUrl` for document 0. That is attribution by receipt order, not proof of whose pixels a frame shows: one received just after a navigation can still show the document before it. Selecting another page or frame does not invalidate an unrelated interval. Handoff pause, connection loss, an uncertain owner and session closure still invalidate all child intervals. A confirmed native stop releases only its own reservation; a failed stop on a live page keeps that target quarantined. A definitively closed page releases its capture reservation. Stopping a child capture does not close its browser. The frame seam has **no website-audio source**, so this package does not synthesize silent samples or infer audio support from a video container. Caller encoding is demonstrated in `examples/record-video.ts`; the example decodes every generated frame with the caller's FFmpeg and checks presentation timestamps and pixel checksums. Native acceptance requires changing pixels and source-time agreement rather than accepting container headers as video evidence. Filming across a navigation with one page-lifetime interval, resampled onto a constant-rate reel with the address of each document reported, is demonstrated in `examples/realistic-footage`.
423
-
424
- ### Read metadata while capture is running
425
-
426
- ```ts
427
- const interval = yield * Capture.start(session, { lifetime: "page" });
428
- const current = yield * interval.snapshot;
429
- // current.phase is "capturing"; initialUrl and recorded documentBoundaries are available now.
430
- ```
431
-
432
- `snapshot` copies the bounded metadata already recorded in host memory. It does
433
- no browser work, consumes no frames, charges no action, and works while the page
434
- is held. Repeated reads neither stop nor restart capture. `observedMonotonicNanos`
435
- stamps the read; each boundary retains its own commit observation time. The
436
- prefix holds at most 64 boundaries; `documentBoundariesTruncated` reports overflow
437
- and `currentDocument` continues counting. Addresses longer than the existing
438
- bound remain `null`. The model should not receive these host-only addresses by
439
- accident merely because it can inspect the page.
440
-
441
- While capturing, `reason` and `nativeStop` are `null`. `phase: "stopping"` means
442
- capture has stopped accepting frames but native cleanup is pending. `"stopped"`
443
- means that cleanup attempt settled; only `nativeStop: "confirmed"` confirms the
444
- native stop. All snapshots are observations of accounting so far: buffered frames
445
- can still be drained after stop. For final accounting, stop or await capture
446
- termination, finish draining `frames`, then read `completed` to obtain the terminal
447
- summary with final delivery counts. Preserve the terminal error and loss counters
448
- when reconciling earlier snapshots. `late` remains included in `dropped`, and
449
- `upstreamDrops` remains `"unknown"`; live metadata adds no stronger pixel or loss
450
- guarantee.
451
-
452
- ### What a compositor is given, and what it owns
453
-
454
- Footage from `capture` is the page surface. It has no pointer, no tab strip and no address bar, so on its own it reads as the inside of a tab rather than as a browser. Drawing those is the application's, exactly as cursor artwork, easing and encoding are: this package has no window-compositing API and will not grow one. What it owes a compositor is the evidence only it can see, on one timeline:
455
-
456
- - each frame's `receivedMonotonicNanos`, and the `document` it was received during;
457
- - an `InputReceipt` for every native pointer and key operation, with the position commanded and an interval on that same clock;
458
- - an address for every document a frame can name: `initialUrl` for document 0, read in the same turn the watch is installed, and a `url` on each boundary, read inside the navigation event that committed it. An application that samples `observe()` between actions can learn an address, but never when it became the address. One longer than 8192 characters is `null`, never cut into an address the page did not show.
459
-
460
- A boundary is the commit, and it is the only moment of a navigation an application cannot see for itself. When the navigation started and when its document finished loading are yours to stamp around the operation that caused it, because you made the call: `startNavigation` returns at dispatch and its `completed` resolves at DOMContentLoaded. The clock is Effect's `Clock`, read where the session was opened and the capture was started, so `Clock.clockWith((clock) => clock.monotonicTimeNanos)` in the same runtime is on the same timeline as every frame and receipt. A title is page state that changes whenever the page likes, not part of a transition: read it with `session.pages` when you need one, and stamp that read yourself.
461
-
462
- The two clocks are never related for you. `sourceTimeMillis` is the browser's wall clock; everything above is the host's monotonic clock; on a hosted session they differ by an offset this package cannot observe. Every frame it receives is already late by the very capture latency it would be trying to measure, and nothing else it is handed carries the browser's time. Relating them takes a round trip into the page, which costs either a charged action or a registered binding, on a schedule only the application can choose, and it fails on a held page. So place input on frames by receipt time, which is always available and late by the capture latency, or measure the offset yourself with a four-timestamp exchange over a typed binding, as NTP does: the page stamps when it called and when the reply arrived, the host stamps when it received and when it replied, and half the best round trip is the error bound to report beside the offset.
463
-
464
- ## Explicit stage-page holds (opt-in)
465
-
466
- Set `pageControl: true` on `BrowserbaseBrowser.layer` to use the host-only `page-control` module. The default remains off. `PageControl.suspend(session, page)` returns a live `PageSuspension`; `PageControl.resume(session, receipt)` consumes that exact receipt. Use a `PageInfo` from `session.pages`. Selection can move to the scout without invalidating the receipt, but connection loss, external target invalidation, completed resume, or another session does invalidate it. Holding or resuming a page may run its `freeze` and `resume` handlers, so nothing observed on _that_ page may be acted on unchecked afterwards: a reference fails `stale` until `session.revalidateElement(reference)` confirms it is still attached and still the control that was inspected. That check sends nothing, never searches for a substitute, and refuses a replaced, detached or changed node. An observation of another page is untouched, so an agent keeps driving the scout while the stage is held. `PageControl.state` reports the last acknowledged local state, not proof about a lost remote connection.
467
-
468
- This opt-in uses maintained CDP attachment with `noDefaults: true` and owner-controlled per-page focus emulation. It intentionally does not support `keepAlive`, reattachment, human handoff, or popup/dialog `pause` policies. These combinations fail before provider allocation; use the existing `retain`/`close` popup policies and `dismiss` dialog policy. Resume explicitly activates the native page without changing SDK selection. Do not enable it where another native client owns focus. Modeled input, DOM reads, waits and viewport changes on held/unknown pages fail before dispatch; the scout remains operable. Page close and session close remain available. Capture does not thaw a held page; frame consumption and acknowledgements never suspend/resume it implicitly.
469
-
470
- The native tests cover page timers, RAF and CSS animation, scout progress with both pages captured, an already-paused animation, and restoration of a non-default rate. Resume waits for one bounded real RAF in an isolated world at rate zero before restoring that rate, avoiding Blink's stale pre-hold animation clock. In-flight callbacks are not undone. Date/wall time, network, media/audio, workers/service workers, and external/provider actions are not promised frozen. This is presentation control, not a security boundary or browser virtual time.
471
-
472
- Partial native failures fence the session as uncertain; there is no success receipt, automatic rollback or retry. Scope cleanup never sends a hidden resume: it closes the owned session/connection. Chromium may reset animation state on CDP detachment, so a previous hold acknowledgement does not guarantee remote clocks remain held after connection loss. Hosted-provider equivalence has not been tested.
267
+ Live frames and page holds follow the common [capture guarantees](../browser/README.md#live-capture-and-presentation). The provider's recording/replay services retain their independent lifetime after the browser scope ends. A downstream encoder consumes public frame streams; it does not own another browser.
473
268
 
474
269
  ## Development and evidence
475
270
 
476
- Repository commands use Vite+: `vp run check`, `vp test`, `vp run install:test-browser`, `vp run test:native`, `vp pack`. Native tests use Playwright 1.63.0 Chromium against loopback HTTP fixtures over the real CDP boundary. Hosted-path fixtures script provider allocation/status behind the real Client; the independent local-owner tests use no account service or provider responses. Unit tests use Effect TestClock, with real-process lifecycle tests explicitly using the live clock. These local boundaries remain distinct from hosted Browserbase evidence: a local CDP pass proves native integration, not provider allocation, Live View authorization, provider recording/audio behavior, recording coexistence, or Browserbase network behavior.
477
-
478
- `tools/packed-consumer.sh` installs the emitted npm tarballs into separate consumers with exact public dependency versions, checks NodeNext declarations, and runs the unchanged native suites without workspace aliases. It also runs real programs directly on Node and Bun: `test/consumer/resources.ts` with no Playwright and no framework installed, and `test/consumer/native.ts` against a real local Chromium.
479
-
480
- ## Independent local Chromium
481
-
482
- `effect-browserbase/local-browser` provides `LocalBrowser`. It uses the same modeled browser owner, native driver, exact-node observations, bootstrap bindings, capture and page control as a hosted session. It requires no Browserbase client, project, key, allocation response or provider endpoint. `BrowserSession<E>` is their shared modeled capability; `BrowserbaseSession<E>` retains the separate provider reference, artifacts, Live View, handoff and release contracts.
483
-
484
- ```ts
485
- import { Effect } from "effect";
486
- import { BrowserPolicy, NavigateRequest } from "effect-browserbase/browser-data";
487
- import { LocalBrowser } from "effect-browserbase/local-browser";
488
-
489
- const program = Effect.scoped(
490
- Effect.gen(function* () {
491
- const browser = yield* LocalBrowser;
492
- const session = yield* browser.open(BrowserPolicy.unrestricted());
493
- yield* session.bind().navigate(NavigateRequest.make({ url: "https://example.com" }));
494
- return yield* session.observe({ scope: "viewport" });
495
- }),
496
- ).pipe(
497
- Effect.provide(
498
- LocalBrowser.layer({
499
- viewport: { width: 1280, height: 720 },
500
- pageControl: true,
501
- launch: { headless: true, chromiumSandbox: true },
502
- }),
503
- ),
504
- );
505
- ```
506
-
507
- Layer construction validates configuration and starts nothing. The optional `playwright-core` peer is loaded only when needed. `acquire(policy, { bootstrap })` starts one owned Chromium process and registers cleanup before waiting for a connection; its cached `connect` yields one `LocalSession<E>`. `open` combines those steps. Bootstrap consumer errors and services remain in the acquisition signatures, just as for hosted sessions. `withBrowser(policy, request, use)` supervises callback failure and checks local cleanup before returning a normal result.
508
-
509
- Owned launch currently supports POSIX hosts (Linux and macOS). It uses a fresh temporary profile, an ephemeral loopback debugger port and one maintained CDP connection. The launcher owns those arguments; callers cannot replace them through `args`. `executablePath` selects an installed Chromium explicitly; otherwise the pinned Playwright executable is used. Headless mode and Chromium sandboxing default to enabled. `chromiumSandbox: false` is an explicit host exception, never inferred from `CI`, root execution or a connection failure. Setting the option alone does not prove the operating system's sandbox configuration. Startup waiting is bounded by `startupTimeoutMillis` (15 seconds by default, at most 60 seconds) and the owner's remaining lifetime. Acquisition does not retry a failed launch.
510
-
511
- For an externally owned Chromium, use `browser.attach(endpoint, { policy, target?, bootstrap? })`. `endpoint` is a `Redacted<string>` containing the exact `ws://127.0.0.1:PORT/devtools/browser/ID` or IPv6-loopback equivalent advertised by that browser. HTTP discovery URLs, non-loopback hosts, credentials, queries and fragments are refused. This validates the control endpoint's shape; it does not authenticate the host running it. A supplied `target.targetId` must identify an existing page. Without one, several candidate pages are an explicit ambiguity error. Attachment preserves the existing viewport and does not replay the layer's launch arguments or create a replacement browser. Coordinate any independent controllers yourself, especially when enabling page control.
512
-
513
- Local identity is `{ provider: "local", id }`, identifying this ownership lifetime. It is not a Browserbase session reference, a PID or an attachment credential. `close` and `cleanupResult` retain `connection` and `process` facts separately. Owned cleanup fences operations, stops capture, disposes initialization, disconnects and terminates its process group; only observed termination permits removing the temporary profile. Failure or timeout remains in `issues` and leaves `process: "unknown"` when exit was not established. Borrowed cleanup disconnects its own client and reports `process: "not-owned"`; it never terminates the external process. Repeated close calls share one result. No local result has a provider `remote: "confirmed"` field. `onCleanup` receives these bounded, host-only facts even when connection setup fails after launch.
514
-
515
- `launch.proxy: { server, bypass? }` forwards an existing host-operated proxy to Chromium. With a proxy, the default bypass value is `<-loopback>` so Chromium does not silently exclude loopback destinations; a different bypass is an explicit host choice. Additional reviewed native flags, such as disabling QUIC and non-proxied WebRTC UDP, can be supplied through `launch.args`. This module does not implement a proxy or qualify its transport/DNS coverage. Browser policy remains `Unrestricted`; a local endpoint, URL admission or successful local test never establishes whole-browser egress containment. Preserve and test the selected enforcing proxy independently. Hosted endpoint validation and network policy are unchanged.
271
+ Read [Contributing](../../CONTRIBUTING.md) for the frozen compatibility workspace and required checks. Provider unit tests cover HTTP/resources and release reconciliation. Cross-package source regression tests live in `test/integration`. The `generic` and `agent-hosted` installed consumers run the maintained native suites against real Chromium with scripted provider HTTP; the `resources` consumer installs no Playwright or Effect Agent. Hosted evidence remains separately recorded in [Status](../../docs/STATUS.md) and never inferred from a local pass.
@@ -1,9 +1,9 @@
1
1
  import { ClientError } from "./Errors.mjs";
2
- import { r as ClientOptions, t as BrowserbaseClient } from "./Client-Ca0zSg1n.mjs";
2
+ import { BrowserbaseClient, ClientOptions } from "./Client.mjs";
3
3
  import { BrowserbaseAgents } from "./Agents.mjs";
4
4
  import { BrowserbaseCertificates } from "./Certificates.mjs";
5
5
  import { BrowserbaseContexts } from "./Contexts.mjs";
6
- import { t as BrowserbaseSessions } from "./Sessions-B8E4kMxo.mjs";
6
+ import { t as BrowserbaseSessions } from "./Sessions-tFJYaXFq.mjs";
7
7
  import { BrowserbaseDownloads } from "./Downloads.mjs";
8
8
  import { BrowserbaseExtensions } from "./Extensions.mjs";
9
9
  import { BrowserbaseFunctions } from "./Functions.mjs";
@@ -15,9 +15,7 @@ import { BrowserbaseSearch } from "./Search.mjs";
15
15
  import { BrowserbaseUploads } from "./Uploads.mjs";
16
16
  import { BrowserbaseWebhooks } from "./Webhooks.mjs";
17
17
  import { Config, Layer } from "effect";
18
- declare namespace Account_d_exports {
19
- export { Services, layer, layerConfig };
20
- }
18
+ //#region src/Account.d.ts
21
19
  /** Every resource service this package owns, on one account's authority. */
22
20
  export type Services = BrowserbaseAgents | BrowserbaseCertificates | BrowserbaseClient | BrowserbaseContexts | BrowserbaseDownloads | BrowserbaseExtensions | BrowserbaseFunctions | BrowserbasePageFetch | BrowserbaseProjects | BrowserbaseRecordings | BrowserbaseReplays | BrowserbaseSearch | BrowserbaseSessions | BrowserbaseUploads | BrowserbaseWebhooks;
23
21
  /**
@@ -30,5 +28,5 @@ export declare const layer: (options: ClientOptions) => Layer.Layer<Services, Cl
30
28
  /** The same bundle, taking account authority from the application's `ConfigProvider`. */
31
29
  export declare const layerConfig: (options?: Omit<ClientOptions, "projectId" | "apiKey">) => Layer.Layer<Services, ClientError | Config.ConfigError>;
32
30
  //#endregion
33
- export { Account_d_exports as t };
31
+ export {};
34
32
  //# sourceMappingURL=Account.d.mts.map
package/dist/Account.mjs CHANGED
@@ -1,11 +1,10 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { t as BrowserbaseClient } from "./Client-D1_xYM9o.mjs";
1
+ import { t as BrowserbaseClient } from "./Client-BQteUePT.mjs";
3
2
  import { BrowserbaseAgents } from "./Agents.mjs";
4
3
  import { BrowserbaseCertificates } from "./Certificates.mjs";
5
4
  import { BrowserbaseContexts } from "./Contexts.mjs";
6
- import { t as BrowserbaseSessions } from "./Sessions-CtbseWEh.mjs";
5
+ import { t as BrowserbaseSessions } from "./Sessions-DoQGg9nB.mjs";
7
6
  import { BrowserbaseDownloads } from "./Downloads.mjs";
8
- import { t as BrowserbaseExtensions } from "./Extensions-ClZbYb1H.mjs";
7
+ import { t as BrowserbaseExtensions } from "./Extensions-nMNMPGCc.mjs";
9
8
  import { BrowserbaseFunctions } from "./Functions.mjs";
10
9
  import { BrowserbasePageFetch } from "./PageFetch.mjs";
11
10
  import { BrowserbaseProjects } from "./Projects.mjs";
@@ -16,10 +15,6 @@ import { BrowserbaseUploads } from "./Uploads.mjs";
16
15
  import { BrowserbaseWebhooks } from "./Webhooks.mjs";
17
16
  import { Layer } from "effect";
18
17
  //#region src/Account.ts
19
- var Account_exports = /* @__PURE__ */ __exportAll({
20
- layer: () => layer,
21
- layerConfig: () => layerConfig
22
- });
23
18
  /**
24
19
  * The services that read one Client. Sessions is provided first and kept in the output,
25
20
  * because the artifact services read it and a caller composing this Layer should not have
@@ -36,6 +31,6 @@ const layer = (options) => services.pipe(Layer.provideMerge(BrowserbaseClient.la
36
31
  /** The same bundle, taking account authority from the application's `ConfigProvider`. */
37
32
  const layerConfig = (options = {}) => services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));
38
33
  //#endregion
39
- export { layer, layerConfig, Account_exports as t };
34
+ export { layer, layerConfig };
40
35
 
41
36
  //# sourceMappingURL=Account.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Account.mjs","names":[],"sources":["../src/Account.ts"],"sourcesContent":["import { type Config, Layer } from \"effect\";\n\nimport { BrowserbaseAgents } from \"./Agents.ts\";\nimport { BrowserbaseCertificates } from \"./Certificates.ts\";\nimport { BrowserbaseClient, type ClientOptions } from \"./Client.ts\";\nimport { BrowserbaseContexts } from \"./Contexts.ts\";\nimport { BrowserbaseDownloads } from \"./Downloads.ts\";\nimport type { ClientError } from \"./Errors.ts\";\nimport { BrowserbaseExtensions } from \"./Extensions.ts\";\nimport { BrowserbaseFunctions } from \"./Functions.ts\";\nimport { BrowserbasePageFetch } from \"./PageFetch.ts\";\nimport { BrowserbaseProjects } from \"./Projects.ts\";\nimport { BrowserbaseRecordings } from \"./Recordings.ts\";\nimport { BrowserbaseReplays } from \"./Replays.ts\";\nimport { BrowserbaseSearch } from \"./Search.ts\";\nimport { BrowserbaseSessions } from \"./Sessions.ts\";\nimport { BrowserbaseUploads } from \"./Uploads.ts\";\nimport { BrowserbaseWebhooks } from \"./Webhooks.ts\";\n\n/** Every resource service this package owns, on one account's authority. */\nexport type Services =\n | BrowserbaseAgents\n | BrowserbaseCertificates\n | BrowserbaseClient\n | BrowserbaseContexts\n | BrowserbaseDownloads\n | BrowserbaseExtensions\n | BrowserbaseFunctions\n | BrowserbasePageFetch\n | BrowserbaseProjects\n | BrowserbaseRecordings\n | BrowserbaseReplays\n | BrowserbaseSearch\n | BrowserbaseSessions\n | BrowserbaseUploads\n | BrowserbaseWebhooks;\n\n/**\n * The services that read one Client. Sessions is provided first and kept in the output,\n * because the artifact services read it and a caller composing this Layer should not have\n * to know which ones do.\n */\nconst services = Layer.mergeAll(\n BrowserbaseAgents.layer,\n BrowserbaseCertificates.layer,\n BrowserbaseContexts.layer,\n BrowserbaseDownloads.layer,\n BrowserbaseExtensions.layer,\n BrowserbaseFunctions.layer,\n BrowserbasePageFetch.layer,\n BrowserbaseProjects.layer,\n BrowserbaseRecordings.layer,\n BrowserbaseReplays.layer,\n BrowserbaseSearch.layer,\n BrowserbaseUploads.layer,\n BrowserbaseWebhooks.layer,\n).pipe(Layer.provideMerge(BrowserbaseSessions.layer));\n\n/**\n * One account for every resource operation, so credentials are composed once rather than\n * per service. Building it performs no I/O, allocates nothing and loads no native peer;\n * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and\n * a connection lifetime that an account does not.\n */\nexport const layer = (options: ClientOptions): Layer.Layer<Services, ClientError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));\n\n/** The same bundle, taking account authority from the application's `ConfigProvider`. */\nexport const layerConfig = (\n options: Omit<ClientOptions, \"projectId\" | \"apiKey\"> = {},\n): Layer.Layer<Services, ClientError | Config.ConfigError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,WAAW,MAAM,SACrB,kBAAkB,OAClB,wBAAwB,OACxB,oBAAoB,OACpB,qBAAqB,OACrB,sBAAsB,OACtB,qBAAqB,OACrB,qBAAqB,OACrB,oBAAoB,OACpB,sBAAsB,OACtB,mBAAmB,OACnB,kBAAkB,OAClB,mBAAmB,OACnB,oBAAoB,KACtB,CAAC,CAAC,KAAK,MAAM,aAAa,oBAAoB,KAAK,CAAC;;;;;;;AAQpD,MAAa,SAAS,YACpB,SAAS,KAAK,MAAM,aAAa,kBAAkB,MAAM,OAAO,CAAC,CAAC;;AAGpE,MAAa,eACX,UAAuD,CAAC,MAExD,SAAS,KAAK,MAAM,aAAa,kBAAkB,YAAY,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"Account.mjs","names":[],"sources":["../src/Account.ts"],"sourcesContent":["import { type Config, Layer } from \"effect\";\n\nimport { BrowserbaseAgents } from \"./Agents.ts\";\nimport { BrowserbaseCertificates } from \"./Certificates.ts\";\nimport { BrowserbaseClient, type ClientOptions } from \"./Client.ts\";\nimport { BrowserbaseContexts } from \"./Contexts.ts\";\nimport { BrowserbaseDownloads } from \"./Downloads.ts\";\nimport type { ClientError } from \"./Errors.ts\";\nimport { BrowserbaseExtensions } from \"./Extensions.ts\";\nimport { BrowserbaseFunctions } from \"./Functions.ts\";\nimport { BrowserbasePageFetch } from \"./PageFetch.ts\";\nimport { BrowserbaseProjects } from \"./Projects.ts\";\nimport { BrowserbaseRecordings } from \"./Recordings.ts\";\nimport { BrowserbaseReplays } from \"./Replays.ts\";\nimport { BrowserbaseSearch } from \"./Search.ts\";\nimport { BrowserbaseSessions } from \"./Sessions.ts\";\nimport { BrowserbaseUploads } from \"./Uploads.ts\";\nimport { BrowserbaseWebhooks } from \"./Webhooks.ts\";\n\n/** Every resource service this package owns, on one account's authority. */\nexport type Services =\n | BrowserbaseAgents\n | BrowserbaseCertificates\n | BrowserbaseClient\n | BrowserbaseContexts\n | BrowserbaseDownloads\n | BrowserbaseExtensions\n | BrowserbaseFunctions\n | BrowserbasePageFetch\n | BrowserbaseProjects\n | BrowserbaseRecordings\n | BrowserbaseReplays\n | BrowserbaseSearch\n | BrowserbaseSessions\n | BrowserbaseUploads\n | BrowserbaseWebhooks;\n\n/**\n * The services that read one Client. Sessions is provided first and kept in the output,\n * because the artifact services read it and a caller composing this Layer should not have\n * to know which ones do.\n */\nconst services = Layer.mergeAll(\n BrowserbaseAgents.layer,\n BrowserbaseCertificates.layer,\n BrowserbaseContexts.layer,\n BrowserbaseDownloads.layer,\n BrowserbaseExtensions.layer,\n BrowserbaseFunctions.layer,\n BrowserbasePageFetch.layer,\n BrowserbaseProjects.layer,\n BrowserbaseRecordings.layer,\n BrowserbaseReplays.layer,\n BrowserbaseSearch.layer,\n BrowserbaseUploads.layer,\n BrowserbaseWebhooks.layer,\n).pipe(Layer.provideMerge(BrowserbaseSessions.layer));\n\n/**\n * One account for every resource operation, so credentials are composed once rather than\n * per service. Building it performs no I/O, allocates nothing and loads no native peer;\n * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and\n * a connection lifetime that an account does not.\n */\nexport const layer = (options: ClientOptions): Layer.Layer<Services, ClientError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));\n\n/** The same bundle, taking account authority from the application's `ConfigProvider`. */\nexport const layerConfig = (\n options: Omit<ClientOptions, \"projectId\" | \"apiKey\"> = {},\n): Layer.Layer<Services, ClientError | Config.ConfigError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,WAAW,MAAM,SACrB,kBAAkB,OAClB,wBAAwB,OACxB,oBAAoB,OACpB,qBAAqB,OACrB,sBAAsB,OACtB,qBAAqB,OACrB,qBAAqB,OACrB,oBAAoB,OACpB,sBAAsB,OACtB,mBAAmB,OACnB,kBAAkB,OAClB,mBAAmB,OACnB,oBAAoB,KACtB,CAAC,CAAC,KAAK,MAAM,aAAa,oBAAoB,KAAK,CAAC;;;;;;;AAQpD,MAAa,SAAS,YACpB,SAAS,KAAK,MAAM,aAAa,kBAAkB,MAAM,OAAO,CAAC,CAAC;;AAGpE,MAAa,eACX,UAAuD,CAAC,MAExD,SAAS,KAAK,MAAM,aAAa,kBAAkB,YAAY,OAAO,CAAC,CAAC"}