effect-browser 0.2.0-beta.7 → 0.2.0-beta.8
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.
- package/README.md +486 -152
- package/dist/Browser-n-bKgBNN.d.mts +372 -0
- package/dist/Browser.d.mts +3 -200
- package/dist/Browser.mjs +9 -1
- package/dist/Browser.mjs.map +1 -1
- package/dist/{BrowserData-KHk4o__c.d.mts → BrowserData-CPIIIHol.d.mts} +81 -7
- package/dist/BrowserData.d.mts +2 -2
- package/dist/BrowserData.mjs +104 -6
- package/dist/BrowserData.mjs.map +1 -1
- package/dist/{BrowserRuntime-hxkTNlRa.d.mts → BrowserRuntime-CFe9uUTv.d.mts} +16 -11
- package/dist/{BrowserRuntime-Cr76CN4I.mjs → BrowserRuntime-HJ1yYlFS.mjs} +4990 -4309
- package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +1 -0
- package/dist/BrowserRuntime.d.mts +2 -2
- package/dist/BrowserRuntime.mjs +2 -1
- package/dist/Capture.d.mts +9 -7
- package/dist/Capture.mjs +157 -32
- package/dist/Capture.mjs.map +1 -1
- package/dist/{CaptureData-Dys0Ko9d.mjs → CaptureData-CYriZHX1.mjs} +25 -6
- package/dist/CaptureData-CYriZHX1.mjs.map +1 -0
- package/dist/CaptureData.d.mts +62 -3
- package/dist/CaptureData.mjs +2 -2
- package/dist/Chromium.d.mts +2 -2
- package/dist/Chromium.mjs +19 -10
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Errors.d.mts +66 -17
- package/dist/Errors.mjs +47 -5
- package/dist/Errors.mjs.map +1 -1
- package/dist/Images-CaaPNeed.mjs +76 -0
- package/dist/Images-CaaPNeed.mjs.map +1 -0
- package/dist/OperationOptions-DAeRS6O9.mjs +55 -0
- package/dist/OperationOptions-DAeRS6O9.mjs.map +1 -0
- package/dist/PageControl.d.mts +6 -6
- package/dist/PageControl.mjs +16 -10
- package/dist/PageControl.mjs.map +1 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +164 -0
- package/dist/PageRegistry-B3q9DS3g.mjs.map +1 -0
- package/dist/Plan-D8qQ48Ic.mjs +512 -0
- package/dist/Plan-D8qQ48Ic.mjs.map +1 -0
- package/dist/Plan.d.mts +3 -0
- package/dist/Plan.mjs +2 -0
- package/dist/PlanData-B668RxbQ.d.mts +4831 -0
- package/dist/PlanData.d.mts +3 -0
- package/dist/PlanData.mjs +268 -0
- package/dist/PlanData.mjs.map +1 -0
- package/dist/PlanInput-BJcPPncs.mjs +75 -0
- package/dist/PlanInput-BJcPPncs.mjs.map +1 -0
- package/dist/PublicSession-Bs61Eal8.mjs +5601 -0
- package/dist/PublicSession-Bs61Eal8.mjs.map +1 -0
- package/dist/Testing.d.mts +48 -6
- package/dist/Testing.mjs +651 -113
- package/dist/Testing.mjs.map +1 -1
- package/dist/Timeline-fV22Xk-S.d.mts +29 -0
- package/dist/Timeline.d.mts +4 -0
- package/dist/Timeline.mjs +20 -0
- package/dist/Timeline.mjs.map +1 -0
- package/dist/TimelineData-BsOn-YGZ.d.mts +3818 -0
- package/dist/TimelineData.d.mts +3 -0
- package/dist/TimelineData.mjs +482 -0
- package/dist/TimelineData.mjs.map +1 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +5 -1
- package/package.json +17 -1
- package/dist/Association-CH3kPHXE.mjs +0 -167
- package/dist/Association-CH3kPHXE.mjs.map +0 -1
- package/dist/BrowserRuntime-Cr76CN4I.mjs.map +0 -1
- package/dist/CaptureData-Dys0Ko9d.mjs.map +0 -1
- package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
- package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ This package has no Browserbase or Effect Agent dependency. Its common entry poi
|
|
|
8
8
|
|
|
9
9
|
A `BrowserSession<E>` is the live, host-only capability returned by the supplying implementation. It preserves callback failures and diagnostics of type `E`, one action budget, one native connection and one set of capture/page-control reservations. `implementation` identifies the control implementation. `closeChecked` performs the owner's cleanup and fails if its required cleanup was not confirmed. On concrete Chromium and Browserbase sessions it returns that same frozen cleanup receipt on success; the generic session permits discarding that value. Helpers that only use operations take `AnySession`, an alias for `BrowserSession<unknown>`. Helpers that supervise callback failures must stay generic in `E` or the concrete session so they retain those failures.
|
|
10
10
|
|
|
11
|
-
All
|
|
11
|
+
All browser operations use issued `Page` and `Frame` objects on the original connection. `Capture.start`, `Capture.stream` and `PageControl` authenticate the exact Page privately; spreading or decoding an object cannot copy authority. An absent registration fails with reason `UnregisteredSession` and outcome `undispatched`. A copy, fabricated value or separately loaded runtime can cause that refusal; it does not establish which occurred. A registered Page with page control disabled still fails `Unsupported`. Tools and adapters bind the issued Page together with its original session for callback supervision and checked owner cleanup. Neither opens another browser.
|
|
12
12
|
|
|
13
|
-
Mutations
|
|
13
|
+
Mutations on one Page share its permit with its Frames; healthy Pages can proceed independently. Registry operations remain serialized. An observed node remains usable only until an invalidating event; a replaced node is never searched for again. A timed-out or interrupted mutation with an unacknowledged native command has an `unknown` outcome and is never automatically replayed. Its exact page is revoked before bounded closure. Positive closure reports `containment: PageClosed` and preserves healthy pages; unconfirmed closure fences the session and reports `SessionFenced`. A page that its own popup/dialog policy paused is kept open for the operator instead, and its unknown work reports `PagePaused`. Containment does not make the original action known. `performed` means dispatched input was acknowledged before a later step failed; it also must not be replayed blindly. `undispatched`, `rejected`, `performed` and `unknown` remain distinct outcomes.
|
|
14
14
|
|
|
15
15
|
## Self-managed Chromium
|
|
16
16
|
|
|
@@ -25,8 +25,8 @@ import { Chromium } from "effect-browser/chromium";
|
|
|
25
25
|
|
|
26
26
|
const program = Browser.scoped(Chromium.launch(BrowserPolicy.unrestricted()), (browser) =>
|
|
27
27
|
Effect.gen(function* () {
|
|
28
|
-
yield* browser.navigate({ url: "https://example.com" });
|
|
29
|
-
return yield* browser.observe({ scope: "viewport" });
|
|
28
|
+
yield* browser.initialPage.navigate({ url: "https://example.com" });
|
|
29
|
+
return yield* browser.initialPage.observe({ scope: "viewport" });
|
|
30
30
|
}),
|
|
31
31
|
).pipe(
|
|
32
32
|
Effect.provide(
|
|
@@ -43,7 +43,7 @@ The Layer requires Effect's `Crypto`, which the host platform supplies: `NodeSer
|
|
|
43
43
|
|
|
44
44
|
`Browser.scoped(open, use)` is the common workflow supervisor for both sources, including borrowed attachment. It runs the acquisition once in its own scope, retains the concrete browser type in `use`, and races workflow completion against typed fail-session callbacks. The workflow has its own child scope: fibers and finalizers finish before `closeChecked` runs, so callback cleanup may still use a healthy browser. Checked closure runs on success, failure, thrown defects and cancellation. A body failure and a cleanup failure both remain in this workflow's own final Effect cause; neither is overwritten. An outer race or timeout can select another result and discard that losing cause even though cleanup ran. Configure the provider's `onCleanup` to record its receipt in a host-owned sink outside the raced workflow when cleanup evidence must survive that composition. A body that completes concurrently with a fail-session callback can still win the race; supervision does not establish failure priority. The ownership finalizer still runs if acquisition itself fails. No failed acquisition or action is replayed.
|
|
45
45
|
|
|
46
|
-
The same combinator supports `open.pipe(Browser.scoped(use))`, including a reusable `const use = Browser.scoped((browser) => browser.observe())` stored before choosing the provider. That unannotated callback sees common browser operations; use an annotated callback or the data-first form for provider-specific members or typed binding diagnostics. The returned function infers the supplying session's callback error independently of its acquisition error. Explicit generic applications must follow the curried overload's revised parameter lists: four outer parameters (`S, A, E2, R2`) and three returned parameters (`E, AE, AR`). It removes the scopes it owns while preserving other required services and error types. Returning a browser, stream or other live capability from `use` does not extend that resource's lifetime. The old provider-specific `withBrowser` methods and `BrowserRuntime.withBrowser` are replaced by this one public operation.
|
|
46
|
+
The same combinator supports `open.pipe(Browser.scoped(use))`, including a reusable `const use = Browser.scoped((browser) => browser.initialPage.observe())` stored before choosing the provider. That unannotated callback sees common browser operations; use an annotated callback or the data-first form for provider-specific members or typed binding diagnostics. The returned function infers the supplying session's callback error independently of its acquisition error. Explicit generic applications must follow the curried overload's revised parameter lists: four outer parameters (`S, A, E2, R2`) and three returned parameters (`E, AE, AR`). It removes the scopes it owns while preserving other required services and error types. Returning a browser, stream or other live capability from `use` does not extend that resource's lifetime. The old provider-specific `withBrowser` methods and `BrowserRuntime.withBrowser` are replaced by this one public operation.
|
|
47
47
|
|
|
48
48
|
### A long-lived session in a Layer
|
|
49
49
|
|
|
@@ -69,13 +69,13 @@ const shared = Layer.effect(
|
|
|
69
69
|
|
|
70
70
|
const program = Effect.gen(function* () {
|
|
71
71
|
const browser = yield* SharedBrowser;
|
|
72
|
-
return yield* browser.observe({ scope: "viewport" });
|
|
72
|
+
return yield* browser.initialPage.observe({ scope: "viewport" });
|
|
73
73
|
}).pipe(Effect.provide(shared));
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Here `recordReceipt` is the host's bounded receipt sink. Consumers of the same Layer build share
|
|
77
77
|
selection, budgets, native admission and lifetime; the Layer does not reset an expired session or
|
|
78
|
-
|
|
78
|
+
give same-Page operations independent permits. Its acquisition finalizer performs unchecked release and
|
|
79
79
|
stores the receipt. It does not supervise `browser.failure`, and does not turn incomplete cleanup
|
|
80
80
|
into a checked-close error for every consumer. The application must choose that supervision and
|
|
81
81
|
explicit checked closure, or use `Browser.scoped` for a bounded workflow that owns those decisions.
|
|
@@ -91,7 +91,7 @@ Place the receipt sink outside that race when those facts must remain observable
|
|
|
91
91
|
let receipt: ChromiumCleanupResult | undefined;
|
|
92
92
|
const result =
|
|
93
93
|
yield *
|
|
94
|
-
Browser.scoped(Chromium.launch(policy), (browser) => browser.navigate({ url })).pipe(
|
|
94
|
+
Browser.scoped(Chromium.launch(policy), (browser) => browser.initialPage.navigate({ url })).pipe(
|
|
95
95
|
Effect.timeoutOption("30 seconds"),
|
|
96
96
|
Effect.provide(
|
|
97
97
|
Chromium.layer({
|
|
@@ -184,47 +184,320 @@ Lost acknowledgement fences control without replay; a late acknowledgement canno
|
|
|
184
184
|
Capacity refusal before dispatch is a known block rather than fabricated uncertainty. Dialog-cap
|
|
185
185
|
overflow follows the same bounded dismissal rules. An acknowledged before-unload dismissal can
|
|
186
186
|
retire only the exact navigation captured when its dialog arrived and subsequently rejected.
|
|
187
|
-
|
|
187
|
+
An attributed popup/dialog pause quarantines its exact Page and stops its capture and bindings;
|
|
188
|
+
healthy peers keep working. The original Page reports `paused` and permits explicit close, while
|
|
189
|
+
input remains refused. Its page timeline ends `stale`, as an all-page pause ends every page's, so a
|
|
190
|
+
run waiting to start on it fails preparation at once. An action the pause interrupts on that page, such as the click that opened
|
|
191
|
+
the dialog, is never closed away: its unknown outcome reports `containment: PagePaused`, and the
|
|
192
|
+
quarantined page's native work does not hold up a handoff's drain. Recovery requires a drained
|
|
193
|
+
session handoff and explicit operator release, then newly acquired Page/Frame authority. Unattributed policy or shared connection failures fence
|
|
194
|
+
the session conservatively.
|
|
188
195
|
|
|
189
196
|
## Browser operations
|
|
190
197
|
|
|
191
|
-
###
|
|
198
|
+
### Page and Frame authority
|
|
199
|
+
|
|
200
|
+
`session.initialPage` is the Page acquired on the first connection. `session.listPages()` returns
|
|
201
|
+
bounded metadata; `session.page(info)` authenticates it and issues the canonical Page for that
|
|
202
|
+
native page and generation. A Page provides navigation, input, observation, exact-node actions,
|
|
203
|
+
checkpoint, readiness, viewport control and close. `page.listFrames()` and `page.frame(info)`
|
|
204
|
+
issue a Frame with the same document operations for that exact frame. None changes display
|
|
205
|
+
selection or opens a connection. An action's result `url` is its frame's http(s) address; a
|
|
206
|
+
document without one (`about:srcdoc`, `about:blank`, `data:`) reports its nearest ancestor
|
|
207
|
+
frame's, which is also its base URL. That address is resolved before dispatch, so an action never
|
|
208
|
+
fails over it after input landed: a target with no http(s) address anywhere in its frame tree, or
|
|
209
|
+
whose own http(s) address cannot be reported (longer than 8,192 characters, or carrying
|
|
210
|
+
credentials; an ancestor's address never stands in for it), is refused `Unsupported` and
|
|
211
|
+
`undispatched`, and an action that removes its own frame or closes its
|
|
212
|
+
page reports the address read before dispatch.
|
|
213
|
+
|
|
214
|
+
Pass an issued Page or Frame as the object you were given. Capture, page control, provider
|
|
215
|
+
transfers, agent tools and `Browser.checkPage` authenticate it by identity, so a spread copy
|
|
216
|
+
(`{ ...page }`), a wrapper or a Proxy is refused `UnregisteredSession`, even though methods copied
|
|
217
|
+
onto it still run. To instrument work, read `session.timeline` and `page.timeline`, which record
|
|
218
|
+
every operation's phases, and plan run receipts, instead of wrapping Page methods.
|
|
192
219
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
```ts
|
|
221
|
+
const stage = session.initialPage;
|
|
222
|
+
const scout = yield * session.createPage();
|
|
223
|
+
yield * scout.navigate({ url: scoutUrl });
|
|
224
|
+
const stageObservation = yield * stage.observe();
|
|
225
|
+
|
|
226
|
+
yield * scout.observe();
|
|
227
|
+
yield * scout.screenshot({ fullPage: false });
|
|
228
|
+
yield *
|
|
229
|
+
stage.clickElement({
|
|
230
|
+
observationId: stageObservation.observationId,
|
|
231
|
+
elementId: stageObservation.controls[0]!.elementId,
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use a control whose inspected role and label match your intent. Each frame has its own current
|
|
236
|
+
observation: reading or acting on the scout preserves the stage's exact references. Another
|
|
237
|
+
observation of the same frame retires its predecessor. A reference passed through the wrong
|
|
238
|
+
Page or Frame fails `Stale/undispatched` before input. Mutation invalidation remains conservative
|
|
239
|
+
for the affected page. A detached frame, closed page or old connection generation cannot acquire
|
|
240
|
+
fresh authority. A refusal names the session's own reason first: while the session is closed,
|
|
241
|
+
expired, fenced or paused, work through any issued Page fails `Closed`, `Expired` or `Busy`, not as
|
|
242
|
+
a stale capability. Otherwise a closed Page fails `Closed`, and a Page from an earlier generation,
|
|
243
|
+
a closing page or a detached frame fails `Stale`, all `undispatched`. `page.status` is passive
|
|
244
|
+
host state, including terminal containment facts, and
|
|
245
|
+
remains readable after closure. Reconnect and handoff resume return a bounded `Inventory` with
|
|
246
|
+
the current generation and fresh Page metadata. Acquire a Page from that inventory and call
|
|
247
|
+
`page.observe()` explicitly; old capabilities stay stale. Native title and URL reads are
|
|
248
|
+
non-atomic. The separate `session.pages` lifecycle stream attaches its cached registry snapshot
|
|
249
|
+
and journal cursor together.
|
|
250
|
+
|
|
251
|
+
Observation storage is finite, including snapshots awaiting native disposal. Optional
|
|
252
|
+
`automation.observationLimits` sets all six bounds together: defaults are 16 snapshots,
|
|
253
|
+
8,192 handles and 64 MiB per page, and 64 snapshots, 32,768 handles and 256 MiB per session.
|
|
254
|
+
Reads reserve capacity before native allocation and return `Limit/undispatched` when it is
|
|
255
|
+
unavailable. Timeout or caller cancellation does not return a reservation until native work
|
|
256
|
+
and disposal settle, or exact positive page/connection retirement proves it unusable.
|
|
257
|
+
The snapshot bounds include temporary reads such as facts, screenshots and checkpoints. Leave
|
|
258
|
+
one slot beyond each retained frame observation for an action's fresh read; a one-slot bound
|
|
259
|
+
supports passive reads alone but cannot read facts or act while an observation occupies it.
|
|
260
|
+
Extraction reserves 4 KiB plus the requested text bytes and 512 KiB per requested control;
|
|
261
|
+
a picture checkpoint also reserves its picture allowance. A standalone screenshot reserves
|
|
262
|
+
4 KiB plus its returned-byte allowance. A plan step's targets reserve, per frame, one
|
|
263
|
+
descriptor read at a time (4 KiB plus 512 KiB) and then what each resolved control actually
|
|
264
|
+
retains, so a large form refuses only when its retained facts exceed the bound. Released native
|
|
265
|
+
resources reduce the retained reservation.
|
|
266
|
+
|
|
267
|
+
`page.ready({ timeoutMillis })`, `page.describe({ timeoutMillis })`,
|
|
268
|
+
`page.listFrames({ timeoutMillis })`, `page.close({ timeoutMillis })` and
|
|
269
|
+
`session.listPages({ timeoutMillis })` narrow the owner's deadline. `Capture.start(page, options)`
|
|
270
|
+
and `Capture.stream(page, options)` use the same owner and capture budget. With page control
|
|
271
|
+
enabled, `PageControl.state(page)`, `PageControl.suspend(page)` and
|
|
272
|
+
`PageControl.resume(page, receipt)` authenticate that exact Page and suspension receipt.
|
|
273
|
+
|
|
274
|
+
### Actions, plans and recording
|
|
275
|
+
|
|
276
|
+
Issued Pages and Frames expose `run(plan, options)` and scoped `start(plan, options)` on their original owner. `effect-browser/plan-data` supplies one tagged action schema for navigation, exact target input, selection, forms, scrolling and bounded waits. A live plan can name an observed Ref or a Descriptor; durable version-1 targets are descriptor-only, with authored literal values or named inputs. Recording replaces input literals with named slots. `Plan.make` validates live intent, `Plan.decode` validates stored intent, and `Plan.encode` serializes normalized defaults. Ingress checks finite data cost before semantic decoding: at most 128 distinct steps and 1 MiB of encoded data.
|
|
196
277
|
|
|
197
278
|
```ts
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
279
|
+
import * as Plan from "effect-browser/plan";
|
|
280
|
+
|
|
281
|
+
// page is an issued Page on the browser's active connection.
|
|
282
|
+
const ran =
|
|
283
|
+
yield *
|
|
284
|
+
page.run(
|
|
285
|
+
{
|
|
286
|
+
version: 1,
|
|
287
|
+
steps: [
|
|
288
|
+
{ id: "open", action: { _tag: "Navigate", url: "https://example.com" } },
|
|
289
|
+
{
|
|
290
|
+
id: "search",
|
|
291
|
+
action: {
|
|
292
|
+
_tag: "Fill",
|
|
293
|
+
target: { _tag: "Descriptor", descriptor: { kind: "input", label: "Search" } },
|
|
294
|
+
value: { _tag: "Input", name: "query" },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "quiet",
|
|
299
|
+
action: { _tag: "Wait", mode: { _tag: "Settled", quietMillis: 100, withinMillis: 2000 } },
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
{ inputs: { query: "Effect" }, within: "20 seconds", style: "plain" },
|
|
304
|
+
);
|
|
305
|
+
const stored = yield * Plan.recorded(ran);
|
|
306
|
+
const encoded = yield * Plan.encode(stored);
|
|
201
307
|
```
|
|
202
308
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
309
|
+
Every step has its own admission and action accounting. Form fields, verification and submit reuse the existing exact-node form implementation; a walk never holds a permit across its steps. `within` captures one absolute run bound, and each step captures its action and queue deadlines once. Resolution, form phases, conditions and optional checkpoints cannot renew those bounds. `through: stepId` returns a successful prefix and the next step's index and id; it does not automatically resume or replay that prefix. Missing input bindings or an invalid `through` fail preparation before any input.
|
|
310
|
+
|
|
311
|
+
Descriptors compare the full exact kind and label, optional destination and stable field metadata, checked ordinal/cardinality and a bounded main-anchored frame path. Scope normalizes to `document`; a recorded viewport descriptor uses document-wide uniqueness by default. Explicit `ViewportContext` resolution requires checked preconditions. Missing, ambiguous, incomplete or exhausted evidence refuses before input. Descriptor steps resolve private exact nodes within the step's admission without replacing the public observation. A live Ref always keeps its original identity and is never substituted.
|
|
312
|
+
|
|
313
|
+
`yield* page.resolve(descriptor, { guard })` performs an explicit admitted read and returns a fresh ordinary Ref. It retires that Frame's previous public observation on success. A Page cannot mint a child Frame's Ref: acquire the checked child Frame and resolve there. A plan on a Page can route a main-anchored child descriptor privately to that exact frame without changing selection.
|
|
314
|
+
|
|
315
|
+
`const operation = yield* page.start(plan, options)` returns a scoped handle before its worker is scheduled. Its `completed` Effect joins one result; repeated joins submit no new input. `operation.cancel` interrupts that worker. `operation.attempts` retains bounded host evidence after cancellation, scope exit or an outer race, including original causes and every native dispatch/acknowledgement phase. Run registrations are bounded to 32 per Page and 128 per session, with at most 1,024 phase rows per step. Evidence overflow prevents complete recording. `StepFailed` distinguishes preparation from an attempted step and retains the completed prefix, current attempt and original `BrowserError`. Its `message` names only the stage, step and reason; the retained steps can hold authored literal values, so log the message rather than the whole error. An acknowledgement survives later verification or postcondition failure; an unknown command stops the walk and keeps the original owner's containment outcome.
|
|
316
|
+
|
|
317
|
+
`Plan.recorded(ran)` projects only completely captured successful intent. Literal fill/type values become deterministic named input slots; existing input names and source conditions remain unchanged. `Plan.inputSlots(plan)` lists every named input a plan reads, with its step, action path and whether it is typed `text` or a replacement `value`, so a host binds `inputs` to replay a recording without the literal ever entering the stored plan. Failed or incomplete whole forms and unknown attempts are not reusable plans. After a later failure, `Plan.recorded(yield* operation.attempts, { through: completedStepId })` can explicitly retain an acknowledged completed prefix. Capture is based on checked native facts, and missing or omitted identity facts produce `Incomplete` rather than executable descriptor intent.
|
|
318
|
+
|
|
319
|
+
`yield* page.settled({ quiet: "100 millis", within: "2 seconds" })` uses one owned wait in the pinned document. It reports quiet DOM mutation, scroll, root geometry and viewport signals. It does not establish network, descendant animation or business completion. Native timers work on hidden pages, but browser throttling can still cause an honest timeout. Cancellation retains wait capacity until native observer disposal is confirmed. A page-initiated navigation that replaces the observed document ends the wait `Stale` and confirms that disposal, so the next wait can start. Plain plans use the same browser inputs as ordinary operations; presentation and recording composition stay with the host.
|
|
320
|
+
|
|
321
|
+
### Replayable session evidence
|
|
322
|
+
|
|
323
|
+
`session.timeline` publishes one bounded metadata journal for the active connection. An issued
|
|
324
|
+
`page.timeline` filters that same journal for its original Page. Snapshot reads and independent
|
|
325
|
+
subscribers acquire no browser permit, send no input and cannot stop native capture.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import * as Timeline from "effect-browser/timeline";
|
|
329
|
+
|
|
330
|
+
const history = yield * session.timeline.snapshot();
|
|
331
|
+
const tail = session.timeline.events(history.resumeAfter);
|
|
332
|
+
const appendTime = yield * session.timeline.now;
|
|
333
|
+
const fromTime = session.timeline.events({ at: appendTime });
|
|
334
|
+
const portable = yield * Timeline.encodeSnapshot(history);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`events(cursor)` resumes exclusively after that sequence; `events({ at })` includes events
|
|
338
|
+
appended at or after the domain-qualified time. `events()` follows from the current tail when
|
|
339
|
+
the stream executes. Snapshots carry immutable events, global available bounds, eviction facts,
|
|
340
|
+
terminal state and `resumeAfter`. A quiet Page's filtered snapshot still uses the global
|
|
341
|
+
watermark, including when it has no matching events. Evicting every retained event does not
|
|
342
|
+
reset that watermark. Requested evicted history and lapped subscribers fail `TimelineGap` with
|
|
343
|
+
requested and available cursors. Foreign store/clock identities and future cursors fail
|
|
344
|
+
`TimelineCursorError`; consumers choose how to recover rather than silently losing evidence.
|
|
345
|
+
An event the journal refuses as malformed or oversized leaves a `MetadataOmitted` marker with
|
|
346
|
+
its original target, correlation and tag, so a Page view sees its own gap. An omission whose
|
|
347
|
+
attribution was itself refused is unattributed and appears in every Page view.
|
|
348
|
+
|
|
349
|
+
`session.pages` is a lifecycle stream: it atomically attaches a copied Inventory baseline from
|
|
350
|
+
the canonical bounded native registry and the journal watermark, then follows lifecycle events.
|
|
351
|
+
Inventory metadata is explicitly cached: unread or omitted title/address fields are `null`.
|
|
352
|
+
It grants no live Page authority. `session.listPages()` remains the admitted fresh native
|
|
353
|
+
metadata read; its URL/title round trips are non-atomic. Restarting `session.pages` obtains a
|
|
354
|
+
fresh baseline after a gap. `Timeline.projectPages` and `Timeline.encodePages` remove cached
|
|
355
|
+
addresses, titles and native target IDs from the client projection. Native navigation and capture
|
|
356
|
+
boundaries retain bounded host addresses observed at their original callbacks; `Timeline.project`
|
|
357
|
+
and `Timeline.encodeClient` strip those nested fields too. `Timeline.encode` and `encodeSnapshot`
|
|
358
|
+
encode host evidence and preserve those addresses.
|
|
359
|
+
|
|
360
|
+
The session facade chooses the current journal when an Effect or Stream executes. Each active
|
|
361
|
+
subscription stays on that journal. Reconnect creates a new store identity; old subscribers
|
|
362
|
+
drain their available tail and terminate, or report a gap if lapped. A time selector earlier
|
|
363
|
+
than the current journal's start fails `TimelineGap`: that history belongs to the previous
|
|
364
|
+
journal, which an issued Page's timeline from that connection still reads. Issued Page timelines stay
|
|
365
|
+
on their original domain. Page/session terminal delivery does not bypass retention, and
|
|
366
|
+
`page.status` retains original containment facts after the corresponding events have evicted.
|
|
367
|
+
|
|
368
|
+
Events include plan correlation, original native phases, genuine input intervals, navigation,
|
|
369
|
+
settled/containment outcomes and capture references. A dispatch means handoff to checked native
|
|
370
|
+
implementation; acknowledgement means native completion. A performed preparatory or burst reply
|
|
371
|
+
carries `acknowledgement: { subphase, logicalComplete }`; only an acknowledgement without that
|
|
372
|
+
fact, or with `logicalComplete: true`, completed logical input. Later follow-up failure remains
|
|
373
|
+
separate. Key events contain counts and their units, never text. Individual pointer commands
|
|
374
|
+
are `Pointer` events; `Glide` is reserved for an actual bounded intended schedule. Capture IDs,
|
|
375
|
+
interval-local frame sequences and boundary attribution refer to the original capture; no
|
|
376
|
+
frame bytes enter the journal. `FirstFrame` means first accepted received frame after a
|
|
377
|
+
boundary, without proving new-document pixels. Picture native-call intervals remain separate
|
|
378
|
+
from DOM/text intervals and screencast receipt times.
|
|
379
|
+
|
|
380
|
+
Phase `nativeOrdinal` counts original dispatch phases within its `operationId`, rather than
|
|
381
|
+
individual CDP commands. `DisplayChanged` carries the selection and qualified running, held or
|
|
382
|
+
unknown display state from the same native callback.
|
|
383
|
+
|
|
384
|
+
Terminal retirement waits for original logical ticket/run outcomes and bounded capture stop
|
|
385
|
+
cleanup to publish their evidence. An unconfirmed native stop remains unconfirmed; retirement
|
|
386
|
+
does not wait indefinitely for raw native promises. Later native callbacks stay pinned to the
|
|
387
|
+
old journal and can be refused once it has retired. Capture `latePhase` qualifies a Watching or
|
|
388
|
+
Started callback after the logical interval ended. Original capture snapshots and quarantine
|
|
389
|
+
retain their independent native stop and loss facts.
|
|
390
|
+
|
|
391
|
+
Append timestamps come from the owner's captured host Clock and order with global sequences.
|
|
392
|
+
Receipt/source times are payload facts and never backdate append order. `Stamp.offsetNanos`
|
|
393
|
+
is relative to that clock domain's origin; its `clockId` is required. Explicit `*Json` schemas
|
|
394
|
+
and Timeline encoding helpers represent bigint offsets/sequences as decimal strings. Native
|
|
395
|
+
presentation Unix milliseconds remain a separately qualified source clock.
|
|
396
|
+
|
|
397
|
+
`timelineLimits` on Chromium/Browserbase options, or `automation.timelineLimits` on the runtime,
|
|
398
|
+
bounds retained metadata and subscriptions. Defaults are 60 seconds, 4,096 events, 4 MiB,
|
|
399
|
+
32 subscribers and 64 KiB per event. Configurable maxima are six hours, 65,536 events, 64 MiB,
|
|
400
|
+
256 subscribers and 1 MiB per event. Metadata-byte and per-event limits have a 2 KiB minimum so
|
|
401
|
+
canonical terminal and omission evidence fits. Count/time/byte eviction runs on append and read. Subscribers
|
|
402
|
+
retain bounded cursor/wakeup state rather than event queues; a slow consumer cannot backpressure
|
|
403
|
+
an admitted native action. Returned values are ordinary immutable snapshots, without an archive
|
|
404
|
+
registry retaining old journals.
|
|
405
|
+
|
|
406
|
+
### Admission and deadlines
|
|
407
|
+
|
|
408
|
+
Ordinary operations on one Page share its permit, including operations on its Frames. Work on
|
|
409
|
+
other Pages can proceed independently: a held scout read or typing response does not block
|
|
410
|
+
stage input, a stage checkpoint or another Page's capture. Creation, inventory and global
|
|
411
|
+
lifecycle operations use the registry permit. Inventory is bounded and non-atomic. So does issuing:
|
|
412
|
+
`session.page(info)` and `page.frame(info)` authenticate and register, so they never wait for, or
|
|
413
|
+
fail `Busy` on, work already running on that page. `page.describe()`, `page.listFrames()` and
|
|
414
|
+
`Capture.start` read the page and share its permit like other reads; pass `admission.queue` to
|
|
415
|
+
wait behind that work.
|
|
416
|
+
|
|
417
|
+
Every admitted operation accepts trailing host-only `OperationOptions`:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
yield * scout.click({ selector: "#advance" }, { admission: { queue: "1 second" } });
|
|
421
|
+
yield * stage.checkpoint({}, { timeoutMillis: 2000, admission: { queue: "500 millis" } });
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Omitting `admission.queue`, or setting it to zero, refuses conflicting work immediately with
|
|
425
|
+
`Busy/undispatched`. A positive finite Effect duration allows FIFO waiting for that permit.
|
|
426
|
+
The queue defaults to 32 pending callers per Page or registry and 128 across the session.
|
|
427
|
+
`automation.admissionLimits.pendingPerPage` and `pendingPerSession` can set either bound to an
|
|
428
|
+
integer from 1 through 1,024. `Chromium.layer` and `BrowserbaseBrowser.layer` accept those fields
|
|
429
|
+
under their top-level `admissionLimits` option. `QueueFull` reports the refusing scope, maximum and observed count;
|
|
430
|
+
`QueueExpired` means the admission wait expired. Cancellation and either refusal dispatch
|
|
431
|
+
nothing and consume no action allowance. A granted waiter owns the permit before waking, so a
|
|
432
|
+
new arrival cannot overtake it.
|
|
433
|
+
|
|
434
|
+
The operation deadline and queue deadline start when the Effect executes. Waiting counts toward
|
|
435
|
+
`timeoutMillis`, the policy deadline and the browser lifetime; admission never renews them.
|
|
436
|
+
The queue deadline bounds only waiting, so admitted work can finish after that deadline. An
|
|
437
|
+
operation deadline that ends first reports `Timeout`. These options do not queue away a stale
|
|
438
|
+
target, a Page hold, a navigation reservation or a native-wait conflict. The issued Page/frame
|
|
439
|
+
authority remains fixed while waiting; display selection cannot redirect queued input.
|
|
440
|
+
`Capture.start` and `Capture.stream` accept admission through their capture options. Exact-node
|
|
441
|
+
methods keep the host's `ElementAdmission` callback separate from trailing operation options.
|
|
442
|
+
|
|
443
|
+
`page.status` includes passive `admission` state: the active operation, pending count and bound,
|
|
444
|
+
oldest wait age, retained native operations, native wait and stop setup. `session.admission` adds
|
|
445
|
+
aggregate counts and registry state. Native work that dispatched input keeps its Page occupied
|
|
446
|
+
after caller cancellation until the actual work settles or exact positive Page/connection
|
|
447
|
+
retirement proves it unusable. A read, or a mutation cancelled before dispatch, can no longer
|
|
448
|
+
change the page: it stays counted in `nativePending` but leaves the Page usable at once.
|
|
449
|
+
These host snapshots grant no priority and expose no native handles.
|
|
450
|
+
|
|
451
|
+
Navigation stopping and Page closure have reserved, bounded cleanup admission, so a full ordinary
|
|
452
|
+
queue cannot prevent recovery. Page closure uses fail-fast reserved admission and its operation
|
|
453
|
+
deadline when joining an existing close; `admission.queue` does not control those waits. Session closure
|
|
454
|
+
preempts work. Handoff, resume and reconnect refuse
|
|
455
|
+
an existing ordinary permit holder before installing their exclusive lifecycle barrier; handoff
|
|
456
|
+
then drains retained native work that can still change a page within its bound before granting
|
|
457
|
+
operator control.
|
|
458
|
+
|
|
459
|
+
### Exact Pages and Frames
|
|
460
|
+
|
|
461
|
+
The session owns lifecycle, display selection and the page registry. `initialPage` is the Page
|
|
462
|
+
issued on the first connection; it remains that original lifetime fact after reconnect. Browser
|
|
463
|
+
input and reads belong to issued Pages and Frames. Constructing or running an operation on one
|
|
464
|
+
never follows display selection:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
const stage = session.initialPage;
|
|
468
|
+
const scout = yield * session.createPage();
|
|
469
|
+
const navigateStage = stage.navigate({ url: stageUrl });
|
|
470
|
+
|
|
471
|
+
yield * session.selectPage(scout);
|
|
472
|
+
yield * navigateStage; // still navigates the original stage
|
|
473
|
+
const seen = yield * scout.observe({ scope: "viewport" });
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
`session.page(pageInfo)` checks both local and native identity under owner admission.
|
|
477
|
+
`createPage()` returns the Page issued for the new page without selecting it. `selectPage(page)`
|
|
478
|
+
takes a Page this session issued and chooses the displayed page, which is also the page `detach`
|
|
479
|
+
records and `reconnect` resumes on; it never retargets a Page or Frame operation, and a closed or
|
|
480
|
+
stale Page is refused unsent. `Browser.checkPage(session, page)` checks that a Page or Frame
|
|
481
|
+
is one this session issued, by identity, and that its authority is still current; adapters call it
|
|
482
|
+
once when they bind, so a foreign, copied or retired target fails before any work. A child frame
|
|
483
|
+
is reached through `page.frame(frameInfo)`, never by selection. Use `page.describe()`, `page.listFrames()`,
|
|
484
|
+
`page.frame(frameInfo)`, `page.resizeViewport(viewport)` and `page.close()` for exact page work.
|
|
485
|
+
The selected-session actions, `retain`, `pinPage`, `pinFrame` and their target-view types are
|
|
486
|
+
removed. The shared operation contract is `PageOperations`.
|
|
487
|
+
|
|
488
|
+
| Previous API | Current API |
|
|
489
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
490
|
+
| Session actions, `observe`, `checkpoint`, `ready`, `target` | Issued `Page`/`Frame` methods and immutable `identity` |
|
|
491
|
+
| `retain`, `pinPage`, `pinFrame`, `selectFrame` | `initialPage`, checked `session.page(info)` and `page.frame(info)` |
|
|
492
|
+
| Session `describePage`, `framesOf`, `closePage`, `resizeViewport` | `page.describe`, `page.listFrames`, `page.close`, `page.resizeViewport` |
|
|
493
|
+
| Session capture and PageInfo capture target | `Capture.start(page)` / `Capture.stream(page)` |
|
|
494
|
+
| Session PageControl adapters | `PageControl.state(page)`, `suspend(page)`, `resume(page, receipt)` |
|
|
495
|
+
| `createPage()` returning `PageInfo` | `createPage()` returns the issued `Page`; `page.describe()` reads its metadata |
|
|
496
|
+
| `selectPage(pageInfo)` | `selectPage(page)` with an issued `Page` |
|
|
497
|
+
|
|
498
|
+
Failed metadata acquisition after creation keeps its original dispatch evidence and never
|
|
499
|
+
creates a second page. Page operations preserve typed failures and scoped ownership; a terminal
|
|
500
|
+
Page's retained status and timeline remain readable, while new work refuses undispatched.
|
|
228
501
|
|
|
229
502
|
Each page `createPage` opens is a window of its own, not a tab. Chromium paints only the front
|
|
230
503
|
tab of a window, so a screenshot of a tab behind another waits seconds for a compositor frame, or
|
|
@@ -234,51 +507,37 @@ and the selected page keeps its capture. With a fixed viewport, each new window'
|
|
|
234
507
|
set to it, as the first page's are; a provider-managed or preserved viewport is left to the
|
|
235
508
|
browser, which sizes a new window like the last one it showed. A browser that answers the
|
|
236
509
|
request for a window with a protocol error, which means it created nothing, gets tabs from then
|
|
237
|
-
on. `createPage`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`describePage(pageInfo)` reads one exact page's current address and title, and whether it is
|
|
242
|
-
selected, without reading every other page. `pages` reads every page's title at once rather than
|
|
243
|
-
one after another, but each title is still a round trip to the browser.
|
|
510
|
+
on. `createPage()` serializes creation and adoption under the registry permit and fails immediately
|
|
511
|
+
when it is occupied. Pass `{ admission: { queue: "1 second" } }` to wait FIFO within the same
|
|
512
|
+
operation deadline. It does not wait behind another Page's ordinary operation. A refused or
|
|
513
|
+
expired caller opens nothing.
|
|
244
514
|
|
|
245
|
-
|
|
515
|
+
Inventory authenticates each native Page before adopting it into the owner's registry,
|
|
516
|
+
including popups. `page.describe()` reads its current address and title without reading every
|
|
517
|
+
other page. `session.listPages()` reads every page's title concurrently under bounded admission.
|
|
518
|
+
Metadata does not itself grant live input authority.
|
|
246
519
|
|
|
247
520
|
```ts
|
|
248
|
-
const stageInfo = (yield * session.
|
|
249
|
-
const stage = yield * session.
|
|
250
|
-
const childInfo = (yield *
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
yield * session.selectPage(scoutInfo);
|
|
521
|
+
const stageInfo = (yield * session.listPages()).find((info) => info.title === "Stage")!;
|
|
522
|
+
const stage = yield * session.page(stageInfo);
|
|
523
|
+
const childInfo = (yield * stage.listFrames()).find((info) => info.parentFrameId !== null)!;
|
|
524
|
+
const child = yield * stage.frame(childInfo);
|
|
525
|
+
|
|
526
|
+
yield * session.selectPage(scout);
|
|
256
527
|
yield * stage.click({ selector: "#advance" });
|
|
257
528
|
const childText = yield * child.readText({ selector: "#status" });
|
|
258
529
|
```
|
|
259
530
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
mean ambiguous. Never fall back to order, local serial, URL or title. Then reacquire frames with
|
|
271
|
-
`framesOf(freshPage)`. A surviving native target can identify the same page, but cannot make old
|
|
272
|
-
local metadata or handles current again.
|
|
273
|
-
|
|
274
|
-
Pinned operations are selector-based. They intentionally do not create another retained
|
|
275
|
-
`Observation` or an exact-node namespace. There is still one observation: a new `observe` replaces
|
|
276
|
-
it. A pinned read or mutation on another page leaves that observation intact, as does selecting
|
|
277
|
-
away and back without observing another document. Exact-node work refuses `Stale/undispatched`
|
|
278
|
-
while another page or frame is selected; it never redirects the reference to that selection.
|
|
279
|
-
Returning to the original page/frame can use the original reference only if its document and
|
|
280
|
-
exact control are still valid. This does not revive a retained-selection handle: `retain` still
|
|
281
|
-
becomes stale after every selection change, including away and back.
|
|
531
|
+
Issuance changes no display selection and opens no second connection. Each operation keeps its
|
|
532
|
+
exact connection-local page/frame authority. Closing its Page, detaching its Frame or reconnecting
|
|
533
|
+
makes it stale. Navigation preserves the Page/Frame authority and retires that document's node
|
|
534
|
+
references. Holds and in-flight navigation reservations are checked on the issued target.
|
|
535
|
+
|
|
536
|
+
IDs are opaque and namespaced per connection. After reconnect, read fresh `listPages()`, match
|
|
537
|
+
exactly one saved native `targetId`, issue `session.page(freshInfo)`, and reacquire its Frames with
|
|
538
|
+
`page.listFrames()` / `page.frame(info)`. Zero matches means gone and multiple matches mean
|
|
539
|
+
ambiguous. Never fall back to ordering, local serial, URL or title. A surviving native target
|
|
540
|
+
cannot make an old Page or copied metadata current again.
|
|
282
541
|
|
|
283
542
|
Dispatched input on the observation's own page still retires its references, including `hover`,
|
|
284
543
|
`pointerMove`, `wheel` and `scroll`. Hover then click therefore deliberately needs a new inspection.
|
|
@@ -288,39 +547,81 @@ even while another page is selected. IDs are opaque and connection-specific; con
|
|
|
288
547
|
inspection result rather than predicting serials. Attachment, identity and fresh-state checks at
|
|
289
548
|
dispatch remain necessary even when the last input targeted a different page.
|
|
290
549
|
|
|
291
|
-
###
|
|
550
|
+
### Performed plans and absolute starts
|
|
551
|
+
|
|
552
|
+
`page.run(plan, { style: {} })` uses the named default performed profile; omitted style or
|
|
553
|
+
`style: "plain"` keeps plain input. `Plan.checkRunOptions(value)` checks and normalizes host
|
|
554
|
+
options, preserving public `within`, queue admission and performed defaults. Profiles have finite
|
|
555
|
+
pointer duration/inset/curvature, key interval/hold and scroll duration/interval bounds. `seed` is
|
|
556
|
+
an optional safe integer; the actual chosen seed is retained in run and attempt receipts.
|
|
557
|
+
Step-local planning depends on that seed and stable step ID, independently of other Pages or
|
|
558
|
+
ambient Random use. It reproduces planned paths for the same geometry, not website behavior or
|
|
559
|
+
remote delivery timing. Slips default to probability zero and require explicit opt-in.
|
|
560
|
+
|
|
561
|
+
The pointer timing uses a chosen bounded Shannon-form policy for measured targets, and explicit
|
|
562
|
+
coordinates use a chosen bounded distance policy. The fifth-order progress polynomial gives zero
|
|
563
|
+
endpoint velocity/acceleration; applying it to a curved policy path does not claim globally minimum
|
|
564
|
+
Cartesian jerk. Constants are library policy, not calibrated human guarantees. Scheduled glides are
|
|
565
|
+
output-only compositor metadata: they do not generate hover along the drawn path. Exact native hit
|
|
566
|
+
checks and selected node identity remain required immediately before real input. A missing actual
|
|
567
|
+
click position stays null even when an intended aim is retained.
|
|
568
|
+
|
|
569
|
+
A schedule has at most 128 geometry samples per path or scroll, and performed text at most 256
|
|
570
|
+
Unicode code points, which expand to at most 768 strokes (a slip adds a wrong key and its
|
|
571
|
+
Backspace), independently of action and pending-native capacity. Longer text fails `Limit` with
|
|
572
|
+
dimension `code-points` and its measured count before any input. Performed Fill
|
|
573
|
+
may focus/select/replace inside its action; Type still requires existing focus. Each performed
|
|
574
|
+
logical action costs the same action budget as its plain counterpart. FillForm retains its
|
|
575
|
+
per-field admission, verification, partial results and guarded submit.
|
|
576
|
+
|
|
577
|
+
`startAt` is an absolute bigint on this owner's host monotonic clock, available through
|
|
578
|
+
`session.monotonicTimeNanos` and, for a host that holds only an issued Page or Frame, the same
|
|
579
|
+
clock's `page.monotonicTimeNanos`. Future starts wait before admission, reserve no Page permit and
|
|
580
|
+
never begin before that instant on the owner's clock, so `latenessNanos` is never negative.
|
|
581
|
+
A waiting start holds only its run registration, never the timeline: when its Page closes or its
|
|
582
|
+
owner pauses, detaches or ends, the run fails preparation at once instead of at its start.
|
|
583
|
+
`within` starts at the intended boundary and bounds queueing, preparation, pacing, input and
|
|
584
|
+
postconditions together, capped by owner lifetime and each action deadline. Missed preparation
|
|
585
|
+
reports `ScheduleMissed` without inventing an attempt; an insufficient pacing budget reports
|
|
586
|
+
`TimingBudgetExceeded` before new input. Performed strokes start at their absolute schedule
|
|
587
|
+
offsets, so a slow reply delays one stroke rather than every later one. Before each stroke's
|
|
588
|
+
first key, the rest of the schedule must still fit the deadline at the round trips the browser
|
|
589
|
+
has actually taken; a stroke that could not finish is refused whole, and strokes already
|
|
590
|
+
acknowledged stay `performed`. Receipts retain requested/intended/actual start, deadline
|
|
591
|
+
and lateness. Cancellation stops future submissions and keeps readable attempt history with the
|
|
592
|
+
original Effect Cause. Known preparatory work followed by refusal is rejected with its subphase
|
|
593
|
+
receipts; pending input remains unknown, and acknowledged logical input stays performed when a
|
|
594
|
+
later check fails.
|
|
595
|
+
|
|
596
|
+
### An unknown outcome on an exact Page
|
|
292
597
|
|
|
293
598
|
A mutation whose outcome becomes unknown after dispatch, because it timed out, failed or was
|
|
294
|
-
interrupted,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
`session.diagnostics` records `page-contained`.
|
|
302
|
-
then finds `status.phase` still `open` knows the page was closed.
|
|
599
|
+
interrupted, never runs again automatically. When the owner can attribute it to an exact Page,
|
|
600
|
+
it revokes that Page's admission and asks the browser to close it within two seconds. This
|
|
601
|
+
includes issued Page/Frame operations, forms, file transfers,
|
|
602
|
+
navigation, initialization and callback retirement. Concurrent close requests join the same
|
|
603
|
+
owned closure attempt. Once the browser confirms closure, healthy Pages, their observations and
|
|
604
|
+
captures can continue. The original operation still fails with its own reason and `unknown`
|
|
605
|
+
outcome, carrying `PageClosed` containment; its Page and Frame authority remains terminal.
|
|
606
|
+
`session.diagnostics` records `page-contained`.
|
|
303
607
|
|
|
304
608
|
Closing the page does not undo what the operation already did. A request it made, a cookie or
|
|
305
609
|
storage it changed, a download or a popup it started can all outlast the page, exactly as they
|
|
306
|
-
can after a known outcome.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
through its own stop.
|
|
312
|
-
|
|
313
|
-
The complete [multi-page example](examples/multi-page.ts) keeps a presentation page pinned while
|
|
610
|
+
can after a known outcome. Unconfirmed closure or a failure without trustworthy Page attribution
|
|
611
|
+
fences the session conservatively and reports `SessionFenced`. A late response never reopens
|
|
612
|
+
fenced control. Containment reports owner safety independently of the original action outcome.
|
|
613
|
+
|
|
614
|
+
The complete [multi-page example](examples/multi-page.ts) keeps an issued presentation Page while
|
|
314
615
|
the selected scout supplies observations. It reads and captures the presentation page without
|
|
315
616
|
switching selection and returns only data after the shared browser scope closes.
|
|
316
617
|
|
|
317
618
|
### Typed host failures
|
|
318
619
|
|
|
319
|
-
`BrowserError` keeps its operation and a required `outcome` (`undispatched`, `rejected`, or
|
|
620
|
+
`BrowserError` keeps its operation and a required `outcome` (`undispatched`, `rejected`, `performed`, or
|
|
320
621
|
`unknown`), while `reason` is a tagged union. Recover by reason without parsing strings:
|
|
321
622
|
|
|
322
623
|
```ts
|
|
323
|
-
const read = session
|
|
624
|
+
const read = session.initialPage
|
|
324
625
|
.readText({})
|
|
325
626
|
.pipe(Effect.catchReason("BrowserError", "Busy", () => Effect.succeed({ text: "" })));
|
|
326
627
|
```
|
|
@@ -336,7 +637,7 @@ terminal escape codes and at most 512 characters, with every address cut to its
|
|
|
336
637
|
(`https://shop.test/…`) and Playwright's call log dropped, since a path, query or DevTools endpoint
|
|
337
638
|
can carry a credential. It is a host diagnostic, and the agent tools never project it.
|
|
338
639
|
`InitializationError` and the provider's separate resource-error families retain their own
|
|
339
|
-
contracts. The Agent tools project host errors to their compact
|
|
640
|
+
contracts. The Agent tools project host errors to their compact model-facing vocabulary and
|
|
340
641
|
retain the originals in the bounded, host-only `ToolHost.toolFailures` snapshot.
|
|
341
642
|
|
|
342
643
|
### A navigation you can watch while it loads
|
|
@@ -344,7 +645,7 @@ retain the originals in the bounded, host-only `ToolHost.toolFailures` snapshot.
|
|
|
344
645
|
`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:
|
|
345
646
|
|
|
346
647
|
Both accept optional `timeoutMillis` from 1 to 600000, capped by the remaining session lifetime.
|
|
347
|
-
Omitting it uses the configured action timeout.
|
|
648
|
+
Omitting it uses the configured action timeout. Page and Frame calls share the
|
|
348
649
|
same navigator; the maintained model `browser_navigate` tool still accepts only its upstream URL
|
|
349
650
|
request. This field is the loading deadline, not a promise of rollback on timeout. On a main-frame
|
|
350
651
|
timeout from the pinned engine, up to 3000 ms of recovery may follow that deadline. One absolute
|
|
@@ -355,9 +656,9 @@ by the remaining session lifetime. It is never renewed and cannot extend that li
|
|
|
355
656
|
const operation =
|
|
356
657
|
yield * handle.startNavigation(StartNavigationRequest.make({ url, timeoutMillis: 30_000 }));
|
|
357
658
|
|
|
358
|
-
const early = yield *
|
|
359
|
-
const receipt = yield * PageControl.suspend(
|
|
360
|
-
yield * PageControl.resume(
|
|
659
|
+
const early = yield * page.checkpoint({ picture: true }); // what has loaded so far
|
|
660
|
+
const receipt = yield * PageControl.suspend(page); // timers, CSS and parsing stop
|
|
661
|
+
yield * PageControl.resume(page, receipt);
|
|
361
662
|
const { url: loaded } = yield * operation.completed;
|
|
362
663
|
```
|
|
363
664
|
|
|
@@ -366,34 +667,36 @@ The owner's permit is released as soon as the navigation is dispatched. While it
|
|
|
366
667
|
- `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.
|
|
367
668
|
- `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. Concurrent callers share their active stop attempt. A busy refusal or cancellation before dispatch allows a later request only after native setup and its port retire; pending or unconfirmed retirement keeps setup capacity occupied. Once a stop is dispatched, every later caller receives that attempt's recorded result, including failure or interruption, without sending it again. A completed navigation cannot stop its successor.
|
|
368
669
|
- A main-frame loading timeout asks the same stop coordinator to retire that exact navigation. Acknowledged stop completes it with `Timeout/unknown` and leaves the owner open: partial content remains inspectable and deliberate subsequent input is allowed. An explicit stop racing recovery shares its attempt; a completed predecessor cannot stop a successor. The `unknown` outcome still says nothing about page effects before cancellation.
|
|
369
|
-
- A failed or unacknowledged recovery, replacement navigation, detached frame or other native
|
|
370
|
-
- Leaving the operation's scope unsettled
|
|
670
|
+
- A failed or unacknowledged recovery, replacement navigation, detached frame or other unknown native outcome closes that exact Page, with a session fence if closure is unconfirmed. Pinned child-frame timeout also uses Page containment: `Page.stopLoading` acts on the whole page, so automatic frame-local cancellation is not claimed. Explicit stop retains its page-wide meaning.
|
|
671
|
+
- Leaving the operation's scope unsettled retains an unknown outcome and closes its Page; healthy Pages survive confirmed closure. Neither navigation nor a dispatched stop is replayed, and no timeout or acknowledgement promises rollback, an unchanged DOM, or termination of every page timer or worker.
|
|
371
672
|
|
|
372
|
-
A read
|
|
673
|
+
A read can inspect a loading Page. If a native read fails because its document was replaced or navigation is in flight, the error is `target-changed` and `undispatched`, which permits another read. A read is never a mutation. A held page is not read at all; take the checkpoint before the hold and keep it.
|
|
373
674
|
|
|
374
675
|
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.
|
|
375
676
|
|
|
376
677
|
### What is on screen, and what a host may know about it
|
|
377
678
|
|
|
378
|
-
`
|
|
679
|
+
`page.observe()` reads the whole document. `page.observe({ scope: "viewport" })` keeps only text and controls that are on screen and reachable, and says what it left out:
|
|
379
680
|
|
|
380
681
|
```ts
|
|
381
|
-
const seen = yield *
|
|
682
|
+
const seen = yield * page.observe({ scope: "viewport" });
|
|
382
683
|
seen.viewport; // { clippedText, coveredText, uncertainText, unreachableControls, exhausted, ... }
|
|
383
684
|
```
|
|
384
685
|
|
|
686
|
+
`viewport.documentScroll`, when present, reports `{ x, y, scrollWidth, scrollHeight, clientWidth, clientHeight }` in CSS pixels. The same reading samples its own Page's or Frame's `window.scrollX/Y` and root `document.scrollingElement` dimensions, so a blank viewport can still report measured progress. Offsets are finite and signed (including negative RTL offsets); dimensions are nonnegative integers, all bounded by `Number.MAX_SAFE_INTEGER`. Client dimensions can differ from `viewport.width/height` because of scrollbars. The field is absent when the document has no scrolling element and in older readings. It reports document layout without exposing offscreen text: nested scrolling containers have their own positions, and these measurements alone do not establish a scroll direction or that the document has ended. Checkpoints carry the same measured geometry without issuing references.
|
|
687
|
+
|
|
385
688
|
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`). Something that takes no pointer events is invisible to that hit test, so for a point beneath one the browser is asked, on the existing connection, which box is on top with pointer events ignored; that also finds boxes inside closed shadow roots. A box that paints at the point, with a fill, border, shadow, filter, image, canvas, SVG, text or a generated `::before`/`::after`, covers the text (`coveredText`); an empty transparent container over the whole page hides nothing. Text the browser could not settle, for example beneath a second such box, in a child frame, or past 256 such points in one reading, 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.
|
|
386
689
|
|
|
387
690
|
`match` narrows a reading, in either scope, to what contains a piece of text, ignoring case: text lines, and controls whose label contains it. A select is kept when its own label or any of its option labels matches, with its options beside it. The filter runs inside the page before `maxControls` and `maxTextBytes` are spent, so twenty header links cannot crowd out the one control asked for:
|
|
388
691
|
|
|
389
692
|
```ts
|
|
390
|
-
const found = yield *
|
|
693
|
+
const found = yield * page.observe({ scope: "document", match: "create account" });
|
|
391
694
|
found.match; // "create account"
|
|
392
695
|
```
|
|
393
696
|
|
|
394
697
|
It filters what is read and never searches for, re-finds or substitutes a node: references still come from the reading itself, with every exact-node check. What a matched reading leaves out is not evidence of absence, which is why it names its `match`.
|
|
395
698
|
|
|
396
|
-
An `Observation` is safe to show a model, and the adapter's `browser_inspect` Tool
|
|
699
|
+
An `Observation` is safe to show a model, and the adapter's `browser_inspect` Tool fits it to its model-result bounds. It 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:
|
|
397
700
|
|
|
398
701
|
Controls also carry optional `checked`, `selected`, `inputType` and `required`. Native checkbox
|
|
399
702
|
and radio state comes from the element; applicable ARIA state accepts only explicit true/false.
|
|
@@ -404,27 +707,27 @@ input decision participates in the exact-node fresh-facts check; field values an
|
|
|
404
707
|
remain excluded from the model projection.
|
|
405
708
|
|
|
406
709
|
```ts
|
|
407
|
-
const facts = yield *
|
|
710
|
+
const facts = yield * page.controlFacts(reference);
|
|
408
711
|
// kind, label, disabled, editable, inputType, autocomplete, formMethod, box, placement, hitTest
|
|
409
712
|
// destination: the resolved link target, or where this control submits its form
|
|
410
713
|
```
|
|
411
714
|
|
|
412
715
|
`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.
|
|
413
716
|
|
|
414
|
-
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
|
|
717
|
+
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` before input, exactly as it does for a replaced or detached node. A performed action also rechecks after delayed preparation; an acknowledged preparatory scroll or focus retains its `rejected` outcome if that later check fails. An existing reference is never substituted by a selector or label lookup. A text-entry control's label never comes from what was entered into it: a textarea, an editable element or a `textbox`, `searchbox`, `combobox` or `spinbutton` role is named only by `aria-label`, `placeholder`, its `<label>` or `aria-placeholder`, so typing neither renames it nor puts the typed text into a recorded descriptor.
|
|
415
718
|
|
|
416
719
|
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:
|
|
417
720
|
|
|
418
721
|
```ts
|
|
419
722
|
yield *
|
|
420
|
-
|
|
723
|
+
page.fillElement(reference, value, {
|
|
421
724
|
admit: (facts) => facts.inputType !== "password" && facts.autocomplete !== "cc-number",
|
|
422
725
|
});
|
|
423
726
|
```
|
|
424
727
|
|
|
425
|
-
Anything but `true`, or a policy that throws, sends
|
|
728
|
+
Anything but `true`, or a policy that throws, sends no further input and fails `denied`. Performed pointer delays and fill preparation re-sample the original exact node and invoke the same policy again before further input. The policy is a plain synchronous function on purpose: it runs while the Page's permit is held, where waiting on a model or a network call would stall other ordinary operations on that Page. It is not an atomic check-and-input transaction, because page script can still run before the native input lands.
|
|
426
729
|
|
|
427
|
-
`fillElement` also refuses, undispatched, what the maintained engine would otherwise refuse only after dispatch, where
|
|
730
|
+
`fillElement` also refuses, undispatched, what the maintained engine would otherwise refuse only after dispatch, where an unknown outcome would close that Page or fence the session if closure is unconfirmed: a hidden control (`not-visible`), a disabled one (`disabled`), one that is not an editable input, textarea or content-editable element, and text that a `number`, `date`, `time`, `range` or other value-typed input would not keep (`unsupported`). The value is checked on a detached copy with the same constraints; the page's own control is not touched until the fill is sent.
|
|
428
731
|
|
|
429
732
|
### Exact native option selection
|
|
430
733
|
|
|
@@ -460,7 +763,7 @@ used for exact-node input applies to the selected control.
|
|
|
460
763
|
```ts
|
|
461
764
|
const result =
|
|
462
765
|
yield *
|
|
463
|
-
|
|
766
|
+
page.fillForm({
|
|
464
767
|
observationId: seen.observationId,
|
|
465
768
|
fields: [
|
|
466
769
|
{ elementId: email, value: "ada@example.test" },
|
|
@@ -474,7 +777,7 @@ const result =
|
|
|
474
777
|
|
|
475
778
|
Each field gives exactly one of `value`, text that replaces the contents of an input, textarea or content-editable element; `checked`, the state a checkbox, radio or switch should end in, clicked only when it differs, and a native radio is never asked to clear itself; or `options`, issued option IDs of a native select exactly as for `selectOption`. A form has at most 32 fields, each control at most once, and its submit control is not also one of its fields.
|
|
476
779
|
|
|
477
|
-
Every step is its own admitted and charged action on the exact observed node, after the same fresh checks as `fillElement`, `selectOption` and `clickElement`, including the host's `admission` policy. One difference is deliberate: a control may have become enabled since it was observed, such as a submit button a form enables once it is complete, but it must be enabled when its step runs. The observation stays usable for this form's own steps only. Navigation, a page hold or any other caller's action still retires it, and the form retires it when it ends.
|
|
780
|
+
Every step is its own admitted and charged action on the exact observed node, after the same fresh checks as `fillElement`, `selectOption` and `clickElement`, including the host's `admission` policy. Each step, the verification read included, captures its own action deadline and queue window when it is requested, so `timeoutMillis` and `admission.queue` bound every step rather than the whole form; inside a plan, the step's captured bounds still cap them all. One difference is deliberate: a control may have become enabled since it was observed, such as a submit button a form enables once it is complete, but it must be enabled when its step runs. The observation stays usable for this form's own steps only. Navigation, a page hold or any other caller's action still retires it, and the form retires it when it ends.
|
|
478
781
|
|
|
479
782
|
After each dispatched step, the page's own handlers get at least two rendered frames and `settleMillis` (50 by default, at most 5000, 0 to skip). A text field is then left, as a person moving on would leave it, so formatting a page applies on blur belongs to that step. Before submit, `verify` (true by default) reads every field again and stops the form when one no longer holds what its own step left there: a re-render, an asynchronous reset, anything that changed a field after it was set. What was read is compared on the host and never returned.
|
|
480
783
|
|
|
@@ -483,7 +786,7 @@ The first refused or uncertain step ends the form, and nothing after it is sent.
|
|
|
483
786
|
### Bounded waits that leave room for recording
|
|
484
787
|
|
|
485
788
|
`waitFor({ selector, state })` still supports the host's bounded selector conditions: `visible`,
|
|
486
|
-
`hidden`, `attached` and `detached`. It captures the
|
|
789
|
+
`hidden`, `attached` and `detached`. It captures the issued page, frame and document when admitted.
|
|
487
790
|
`waitForElement({ reference, state, timeoutMillis? })` instead waits on an exact node issued by the
|
|
488
791
|
current observation, with `visible`, `hidden`, `enabled` or `disabled`. It never searches for a node
|
|
489
792
|
that replaced the reference. Hidden includes detachment of the original node; for the other
|
|
@@ -498,13 +801,12 @@ the configured action timeout and remaining lifetime still cap it, including set
|
|
|
498
801
|
can move elsewhere without retargeting the wait. Its condition is a sample, not a reservation
|
|
499
802
|
that the page will remain unchanged.
|
|
500
803
|
|
|
501
|
-
While a wait is pending, `checkpoint`, `
|
|
804
|
+
While a wait is pending, `checkpoint`, `listPages` and ordinary bounded reads can use the normal
|
|
502
805
|
permit. Mutations, another navigation and page holds on the waited page are refused before
|
|
503
|
-
dispatch. Input on another page may proceed; replacing the
|
|
504
|
-
the logical wait ends. Closing a page or session remains available and cancels affected waits.
|
|
806
|
+
dispatch. Input and observation on another page may proceed; replacing the waited frame's observation is refused until the logical wait ends. Closing a page or session remains available and cancels affected waits.
|
|
505
807
|
No general read-under-write bypass has been added.
|
|
506
808
|
|
|
507
|
-
One native wait may be outstanding per
|
|
809
|
+
One native wait may be outstanding per exact Page, with a finite session bound of 32. Cancellation and timeout release the
|
|
508
810
|
logical barrier without inventing an uncertain mutation, but retain native capacity until the
|
|
509
811
|
actual native promise and any required handle disposal settle, or that exact connection retires.
|
|
510
812
|
Consequently another wait can still receive `Busy/undispatched` after its predecessor's caller
|
|
@@ -514,10 +816,10 @@ the observation is replaced, and its late cleanup cannot dispose a successor's r
|
|
|
514
816
|
|
|
515
817
|
### Passive checkpoints for a recorder
|
|
516
818
|
|
|
517
|
-
`observe()` replaces
|
|
819
|
+
`page.observe()` replaces that frame's current observation, so a recorder calling it on the same frame would retire the references an agent is about to use. `page.checkpoint()` is the passive path: viewport text, control facts and, when asked, a PNG of the viewport.
|
|
518
820
|
|
|
519
821
|
```ts
|
|
520
|
-
const checkpoint = yield *
|
|
822
|
+
const checkpoint = yield * page.checkpoint({ picture: true });
|
|
521
823
|
```
|
|
522
824
|
|
|
523
825
|
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 held page is refused `busy` rather than woken to be read: take the checkpoint before the hold and keep it.
|
|
@@ -526,7 +828,7 @@ It issues no references, is not a mutation, and leaves the action observation an
|
|
|
526
828
|
`Chromium.layer({ maxHostReads })` or the corresponding provider Layer. The default is 10,000,
|
|
527
829
|
with an explicit integer bound of 1–1,000,000; invalid values are refused rather than clamped.
|
|
528
830
|
Exhaustion reports `Limit { dimension: "host-reads", maximum, observed } / undispatched` while the
|
|
529
|
-
owner stays open. These operations still obey the same lifetime, bytes, deadline and
|
|
831
|
+
owner stays open. These operations still obey the same lifetime, bytes, deadline and admission
|
|
530
832
|
concurrency bounds: a checkpoint can execute page script and is not unlimited free work.
|
|
531
833
|
`observe`, `readText`, `screenshot` and `waitFor` continue consuming the model-reachable action
|
|
532
834
|
allowance, which `status.actions` reports. Neither budget is a tool parameter, and `maxActions`
|
|
@@ -537,7 +839,7 @@ has not become mutations-only.
|
|
|
537
839
|
`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.
|
|
538
840
|
|
|
539
841
|
```ts
|
|
540
|
-
const handle = session; //
|
|
842
|
+
const handle = session.initialPage; // exact original Page authority
|
|
541
843
|
|
|
542
844
|
yield * handle.pointerMove(PointerMoveRequest.make({ to: { x: 140, y: 100 } }));
|
|
543
845
|
yield * handle.hover(HoverRequest.make({ selector: "#menu" }));
|
|
@@ -546,9 +848,9 @@ const receipt =
|
|
|
546
848
|
yield * handle.wheel(WheelRequest.make({ deltaX: 0, deltaY: 240, at: { x: 420, y: 120 } }));
|
|
547
849
|
```
|
|
548
850
|
|
|
549
|
-
Coordinates are CSS pixels in the main frame's viewport. Each
|
|
851
|
+
Coordinates are CSS pixels in the main frame's viewport. Each logical input is one action on its issued Page or Frame. Performed plans add bounded pacing and output-only glide evidence; viewers draw cursor artwork from projected evidence without injecting presentation DOM into the website.
|
|
550
852
|
|
|
551
|
-
`hover` places the pointer on one exact element where it is, by selector or by the node an observation named (`
|
|
853
|
+
`hover` places the pointer on one exact element where it is, by selector or by the node an observation named (`page.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`. A performed hover aims inside the part of the element its viewport and overflow ancestors leave visible, so it reaches a partly clipped element a plain hover reaches, and refuses only one with no visible part.
|
|
552
854
|
|
|
553
855
|
For a child-frame element, hover checks the commanded point through each ancestor
|
|
554
856
|
frame and then the exact node, including cross-origin documents. The receipt
|
|
@@ -566,7 +868,7 @@ An `InputReceipt` carries the target it was sent to, the position this owner com
|
|
|
566
868
|
`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.
|
|
567
869
|
|
|
568
870
|
```ts
|
|
569
|
-
const handle = session; //
|
|
871
|
+
const handle = session.initialPage; // exact original Page authority
|
|
570
872
|
|
|
571
873
|
// Focus is the page's business. A real click gives it, and keys then follow it.
|
|
572
874
|
yield * handle.click(ClickRequest.make({ selector: "#from" }));
|
|
@@ -577,19 +879,38 @@ yield * handle.press(PressRequest.make({ key: "k", modifiers: ["Control"] }));
|
|
|
577
879
|
yield * handle.press(PressRequest.make({ key: "Enter", into: "#from" }));
|
|
578
880
|
```
|
|
579
881
|
|
|
580
|
-
Keys go to whatever has focus in the
|
|
882
|
+
Keys go to whatever has focus in the issued 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. `page.pressElement` and `page.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.
|
|
581
883
|
|
|
582
884
|
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.
|
|
583
885
|
|
|
584
|
-
`type` sends up to 256 characters as one charged action
|
|
886
|
+
`type` sends up to 256 characters as one charged action under a single action timeout. It submits ordered native input in windows of at most 16 code points and 32 commands, without waiting for every individual reply. Each normal stroke ends at key-up, and the window drains before another begins. The owner is checked before every command, including key-up: a fence stops unsent input, and no interrupted run is replayed or repaired. Failure while dispatched input is still unacknowledged leaves an `unknown` outcome; the owner closes that exact page, or fences the session when closure is unconfirmed. Failure after all earlier input was acknowledged reports `performed`. Neither result replays the run. A canceled caller does not release native typing capacity; unresolved attachment, replies or failed port cleanup retain it until confirmed retirement. A run whose replies all succeeded leaves its private port attached for the next run on the same Page. Each Page owns its port, with at most 32 retained ports across the connection; switching Pages does not detach another Page's idle port. Positive Page closure or connection retirement releases its capacity.
|
|
585
887
|
|
|
586
|
-
|
|
888
|
+
With `into`, the original node and document must still have focus before each subsequent window. Focus is never repaired. Commands already submitted in a window can land after focus moves. Once that window has successfully drained, a later `not-focused` refusal reports `performed`: earlier input was acknowledged, and the refused window sends nothing.
|
|
889
|
+
|
|
890
|
+
Performed typing checks focus once before each stroke, never between a key and its release: a
|
|
891
|
+
key whose own default action moves focus (Tab, an Enter that submits, an auto-advancing field)
|
|
892
|
+
still completes its stroke. A fully acknowledged stroke, including its key and modifier releases,
|
|
893
|
+
retains `performed` evidence if a later stroke is refused. An acknowledged text insertion, the
|
|
894
|
+
Backspace that erases a performed Fill's old value, a filled form field and an acknowledged
|
|
895
|
+
scroll sample do the same. Only focus, selection and scroll-into-view preparation remain partial
|
|
896
|
+
and report `rejected` when nothing else landed; unresolved key input still reports `unknown` and
|
|
897
|
+
receives the original containment policy. A later failure never weakens what was acknowledged.
|
|
898
|
+
|
|
899
|
+
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. Plain `type` sends a shifted character 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`. Performed plans hold Shift for uppercase and shifted punctuation and pace complete balanced strokes under one logical action. A stroke remains unresolved until its key and modifiers are released; quiet intervals do not renew the absolute deadline or retain unresolved native replies.
|
|
900
|
+
|
|
901
|
+
A press waits for native input acknowledgement, without waiting for resulting navigation. 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. Both operations are available in the model-facing toolkit in `effect-agent-browser`.
|
|
587
902
|
|
|
588
903
|
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.
|
|
589
904
|
|
|
590
|
-
Persistent Browserbase contexts require a live writer permit from `ContextCoordination.withWriter` when writes are persisted. Detach/reconnect is opt-in with `keepAlive
|
|
905
|
+
Persistent Browserbase contexts require a live writer permit from `ContextCoordination.withWriter` when writes are persisted. Detach/reconnect is opt-in with `keepAlive`. Detach first lets admitted work finish within a bounded interval, such as a page close admitted behind its barrier or a started navigation that is still loading, rather than disconnecting beneath it; work that cannot finish fails the detach as `Timeout/undispatched` and leaves the session open. 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.
|
|
591
906
|
|
|
592
|
-
Human handoff
|
|
907
|
+
Human handoff blocks new admission and drains native work that can still change a page within a
|
|
908
|
+
bounded interval before pausing automation and returning host-only Live View material. Pure waits
|
|
909
|
+
and reads do not hold it up; the pause refuses them as `Stale`. Work that cannot drain in time,
|
|
910
|
+
such as a navigation still loading, fails the request with `Timeout/undispatched`: no handoff is
|
|
911
|
+
granted, the barrier lifts and automation continues. Resume requires an explicit operator-release signal
|
|
912
|
+
and returns fresh bounded Page inventory under the lifecycle permit; observing a newly acquired
|
|
913
|
+
Page is explicit. Once the pause is installed, a failed Live View authorization 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.
|
|
593
914
|
|
|
594
915
|
## Registrations, capabilities and document readiness
|
|
595
916
|
|
|
@@ -636,9 +957,9 @@ const run = Browser.scoped(
|
|
|
636
957
|
Chromium.launch(BrowserPolicy.unrestricted(), { bootstrap }),
|
|
637
958
|
(session) =>
|
|
638
959
|
Effect.gen(function* () {
|
|
639
|
-
yield* session.navigate({ url: "https://portal.example.com" });
|
|
640
|
-
yield* session.ready;
|
|
641
|
-
return yield* session.observe();
|
|
960
|
+
yield* session.initialPage.navigate({ url: "https://portal.example.com" });
|
|
961
|
+
yield* session.initialPage.ready();
|
|
962
|
+
return yield* session.initialPage.observe();
|
|
642
963
|
}),
|
|
643
964
|
);
|
|
644
965
|
// run requires Chromium and ShowSettings. Browser.scoped discharges both the
|
|
@@ -651,23 +972,23 @@ Each binding accepts exactly one JSON-compatible argument and returns the output
|
|
|
651
972
|
|
|
652
973
|
Plans admit at most 16 uniquely named bindings. Omitted binding options default to `maxConcurrent: 1`, `maxInputBytes: 65536`, `maxOutputBytes: 65536`, `timeoutMillis: 10000` and `failureMode: "reject-call"`. Name, exact origins, codecs and handler remain required. Explicit bounds/mode pass the same validated registration path; zero, null or excessive values are rejected, never clamped. `combine` selects the most conservative `existingDocuments` policy across its scripts, even when their origin sets differ. 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.
|
|
653
974
|
|
|
654
|
-
`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 `BrowserBindingError: 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
|
|
975
|
+
`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. A call from a document outside the binding's origins is refused before admission under either mode: it takes no capacity, never reaches the codecs or the handler, and cannot fail the session. It is counted as rejected and recorded as a `reject-call` failure with reason `origin`. Pages receive only `BrowserBindingError: 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. Reentrant browser work on that same Page follows ordinary admission: it fails `busy` and `undispatched` by default, or waits under an explicit finite queue. Other healthy Pages can proceed independently.
|
|
655
976
|
|
|
656
977
|
`Browser.scoped` supervises fail-session errors and requires the owner's checked cleanup. Explicit `acquire`/`launch` 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 invokes the supplying lifetime’s release when 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.
|
|
657
978
|
|
|
658
|
-
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.
|
|
979
|
+
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.initialPage.ready()` reports the current document without charging an action, and an origin outside the plan is reported as `NotApplicable` rather than waited on.
|
|
659
980
|
|
|
660
981
|
The reviewed permission subset is exercised against real Chromium. Provider extensions, persistent contexts and provider reconnect evidence belong to the supplying integration; see the [Browserbase guide](../browserbase/README.md).
|
|
661
982
|
|
|
662
983
|
## Live capture and presentation
|
|
663
984
|
|
|
664
|
-
For a frame-processing pipeline, use `Capture.stream(
|
|
985
|
+
For a frame-processing pipeline, use `Capture.stream(page, options)`. It starts only when consumed and owns one interval per subscription. `Stream.take`, consumer failure and interruption all finish that interval before the stream completes, while the enclosing browser stays open. A new subscription creates a new interval; concurrent subscriptions on the same page still fail `busy` under the existing reservation. Capture bounds and native-stop quarantine rules are unchanged.
|
|
665
986
|
|
|
666
987
|
```ts
|
|
667
988
|
import { Stream } from "effect";
|
|
668
989
|
import * as Capture from "effect-browser/capture";
|
|
669
990
|
|
|
670
|
-
const firstFrame = Capture.stream(
|
|
991
|
+
const firstFrame = Capture.stream(page, { lifetime: "page" }).pipe(
|
|
671
992
|
Stream.take(1),
|
|
672
993
|
Stream.runCollect,
|
|
673
994
|
);
|
|
@@ -695,7 +1016,7 @@ import { Clock, Effect, Stream } from "effect";
|
|
|
695
1016
|
|
|
696
1017
|
const interval =
|
|
697
1018
|
yield *
|
|
698
|
-
Capture.start(
|
|
1019
|
+
Capture.start(page, {
|
|
699
1020
|
lifetime: "page",
|
|
700
1021
|
size: { width: 1280, height: 720 },
|
|
701
1022
|
maxFrames: 400,
|
|
@@ -714,14 +1035,14 @@ const delayed = interval.frames.pipe(
|
|
|
714
1035
|
|
|
715
1036
|
`Capture.multipart(frames)` turns frames into one `multipart/x-mixed-replace` response for an `<img>`, the motion JPEG that WHATWG HTML defines for images. It closes each frame with the next part's delimiter and headers at once, because a browser shows a part only when it has read the headers of the one after it: without that, a viewer runs one frame behind and never shows the last picture of a page that has gone still. It writes no metadata, so nothing but pictures reaches a viewer, and it draws a new boundary from `Crypto` for each response. Call it once per viewer, over one fan-out of the interval, for example a sliding `PubSub` with `replay: 1` so that a slow viewer skips frames without slowing anyone else and a new one is shown the current picture. End that fan-out before the HTTP server stops: a server waits for its open responses.
|
|
716
1037
|
|
|
717
|
-
Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(
|
|
1038
|
+
Closing, navigating, detaching a relevant frame, or resizing the captured page ends its interval explicitly without ending a sibling page's capture. `Capture.start(page, { lifetime: "page" })` instead follows a page's main frame across documents: start it before a navigation and it covers the loading in between. Same-document URL changes leave document-lifetime capture and observations active. A page-lifetime interval records those changes with `sameDocument: true` and keeps the current document number; only a cross-document commit advances it. 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 new document), and the summary's bounded `documentBoundaries` give the last sequence before each URL change and the address it reached, 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 until a late stop confirms or the page definitively closes. 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 [the caller encoder example](../browserbase/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 [the footage example](../browserbase/examples/realistic-footage/README.md).
|
|
718
1039
|
|
|
719
1040
|
Same-document detection uses the pinned Playwright 1.63.0 client `Frame` event `navigated`: `newDocument` is absent for URL-only changes, and the client emits this event synchronously before `Page` emits `framenavigated`. A runtime missing that private signal fails explicitly rather than treating URL changes as document commits.
|
|
720
1041
|
|
|
721
1042
|
### Read metadata while capture is running
|
|
722
1043
|
|
|
723
1044
|
```ts
|
|
724
|
-
const interval = yield * Capture.start(
|
|
1045
|
+
const interval = yield * Capture.start(page, { lifetime: "page" });
|
|
725
1046
|
const current = yield * interval.snapshot;
|
|
726
1047
|
// current.phase is "capturing"; initialUrl and recorded documentBoundaries are available now.
|
|
727
1048
|
```
|
|
@@ -735,6 +1056,13 @@ ones were let go, and `currentDocument` continues counting. Addresses longer tha
|
|
|
735
1056
|
bound remain `null`. The model should not receive these host-only addresses by
|
|
736
1057
|
accident merely because it can inspect the page.
|
|
737
1058
|
|
|
1059
|
+
Every summary and snapshot includes `qualification`: this exact target's authority phase,
|
|
1060
|
+
containment facts, and the owner's phase and generation. A revoked Page may still be awaiting
|
|
1061
|
+
closure; `PageClosed` appears only after positive native closure, independently of `nativeStop`.
|
|
1062
|
+
Later `snapshot` and `completed` reads can retain that confirmation even when capture ended
|
|
1063
|
+
earlier. The original action outcome and capture end reason remain unchanged. A terminal Page
|
|
1064
|
+
requires explicitly fresh authority and intent before another capture can start.
|
|
1065
|
+
|
|
738
1066
|
While capturing, `reason` and `nativeStop` are `null`. `phase: "stopping"` means
|
|
739
1067
|
capture has stopped accepting frames but native cleanup is pending. `"stopped"`
|
|
740
1068
|
means that cleanup attempt settled; only `nativeStop: "confirmed"` confirms the
|
|
@@ -754,19 +1082,19 @@ Footage from `capture` is the page surface. It has no pointer, no tab strip and
|
|
|
754
1082
|
- an `InputReceipt` for each exposed native pointer, click and key operation, with a known position when the owner has one and an interval on that same clock. Internal download and file-chooser clicks clear the remembered pointer position but do not expose a receipt;
|
|
755
1083
|
- 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 cross-document boundary, read inside the navigation event that committed it. Page-lifetime captures separately mark same-document URL changes without advancing the document number. 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.
|
|
756
1084
|
|
|
757
|
-
A cross-document boundary is the commit, and it is the only moment of a navigation an application cannot see for itself. A same-document boundary records a URL change without claiming that a new document committed. When a cross-document 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.
|
|
1085
|
+
A cross-document boundary is the commit, and it is the only moment of a navigation an application cannot see for itself. A same-document boundary records a URL change without claiming that a new document committed. When a cross-document 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. Frames and receipts use the session's captured Effect `Clock`; a later caller Clock override does not change their domain. Capture `yield* Clock.Clock` when opening the session if you need raw host stamps on that same clock. `session.timeline.now` supplies the qualified offsets used by journal intervals. A title is page state that changes whenever the page likes, not part of a transition: read it with `page.describe()` when you need one, and stamp that read yourself.
|
|
758
1086
|
|
|
759
1087
|
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.
|
|
760
1088
|
|
|
761
1089
|
## Explicit stage-page holds (opt-in)
|
|
762
1090
|
|
|
763
|
-
Set `pageControl: true` on `Chromium.layer` or `BrowserbaseBrowser.layer` to use the host-only `page-control` module. The default remains off. `PageControl.suspend(
|
|
1091
|
+
Set `pageControl: true` on `Chromium.layer` or `BrowserbaseBrowser.layer` to use the host-only `page-control` module. The default remains off. `PageControl.suspend(page)` returns a live `PageSuspension`; `PageControl.resume(page, receipt)` consumes that exact receipt. Use an issued Page from `session.page(info)` or `initialPage`. 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 `page.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.
|
|
764
1092
|
|
|
765
1093
|
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 acquisition; 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.
|
|
766
1094
|
|
|
767
1095
|
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.
|
|
768
1096
|
|
|
769
|
-
|
|
1097
|
+
Unknown partial native failures close the exact Page, preserving healthy peers after confirmed closure; unconfirmed closure fences the session. 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.
|
|
770
1098
|
|
|
771
1099
|
## Network policy
|
|
772
1100
|
|
|
@@ -778,6 +1106,8 @@ Only explicit trusted-host `Unrestricted` network policy is implemented. `ExactH
|
|
|
778
1106
|
|
|
779
1107
|
A script lists documents by address, each with its text and the controls it offers. A control's `id` becomes its issued `elementId`, and observations are numbered `observation-1`, `observation-2`, …, so a scripted model turn or a stored expectation can name a node statically. A `link` or a submit control carries a `destination`, which is both its host-only destination fact and where activating it leads; a plain button navigates through `activates` and reports no destination, as a real `onclick` handler has none, and the schema refuses a destination where a real document would report none. Navigating to an unlisted address reaches an empty document at that address. A checkbox or radio `input` without `checked` starts unchecked, as a real one does. `fillForm` and matched readings take the same steps over the script: a text step records its value in `document.values` and leaves the control, a toggle is clicked only when it differs, options are the observation's issued option IDs, and `next("fill-form", …)` arms the next step, verification read or submit.
|
|
780
1108
|
|
|
1109
|
+
An optional document `documentScroll` supplies an explicit geometry snapshot with the same six fields as a reading. `observe`, `checkpoint` and `document.current` copy those facts; `document.update` supplies a subsequent snapshot. Scripted scroll and resize operations do not infer layout, clamping or RTL origins from those numbers. Omit the field when geometry is unknown. Authored snapshots establish the owner's behavior over those facts, not native layout.
|
|
1110
|
+
|
|
781
1111
|
```ts
|
|
782
1112
|
import { expect, it } from "@effect/vitest";
|
|
783
1113
|
import { Effect } from "effect";
|
|
@@ -814,13 +1144,13 @@ it.effect("an unknown click is never replayed", () =>
|
|
|
814
1144
|
reason: Reasons.Timeout.make({}),
|
|
815
1145
|
outcome: "unknown",
|
|
816
1146
|
});
|
|
817
|
-
const observation = yield* browser.observe();
|
|
1147
|
+
const observation = yield* browser.initialPage.observe();
|
|
818
1148
|
const accept = { observationId: observation.observationId, elementId: "accept" };
|
|
819
1149
|
|
|
820
|
-
const first = yield* browser.clickElement(accept).pipe(Effect.flip);
|
|
1150
|
+
const first = yield* browser.initialPage.clickElement(accept).pipe(Effect.flip);
|
|
821
1151
|
|
|
822
1152
|
expect(first).toMatchObject({ reason: { _tag: "Timeout" }, outcome: "unknown" });
|
|
823
|
-
const retry = yield* browser.clickElement(accept).pipe(Effect.flip);
|
|
1153
|
+
const retry = yield* browser.initialPage.clickElement(accept).pipe(Effect.flip);
|
|
824
1154
|
|
|
825
1155
|
expect(retry).toMatchObject({ reason: { _tag: "Closed" }, outcome: "undispatched" });
|
|
826
1156
|
const clicks = (yield* browser.control.calls).filter((c) => c.operation === "click");
|
|
@@ -832,7 +1162,7 @@ it.effect("an unknown click is never replayed", () =>
|
|
|
832
1162
|
);
|
|
833
1163
|
```
|
|
834
1164
|
|
|
835
|
-
`browser.control` is the test's side of the scripted browser. The browser outlives any one connection, as a keep-alive browser does: a reconnection to the same address finds the pages it left, and the control keeps working while no connection is open, so a test can change a page while its owner is detached. `connections` lists every connection made to that browser and how each ended so far (`open`, `closed`, `dropped`, `close-failed`, or `refused` by the script's `connections: ["refuse", …]`), which makes a connection an owner left open visible. `next("disconnect", …)` scripts the owner's native teardown: `Fail` makes it fail, so the receipt reports `connection: "failed"` with a `disconnect` issue, and `Hold` parks it at a gate however long cleanup waits. A navigation stop is recorded in `calls` as `navigate-stop`, and its arms apply before or after it is sent. Closing
|
|
1165
|
+
`browser.control` is the test's side of the scripted browser. The browser outlives any one connection, as a keep-alive browser does: a reconnection to the same address finds the pages it left, and the control keeps working while no connection is open, so a test can change a page while its owner is detached. `connections` lists every connection made to that browser and how each ended so far (`open`, `closed`, `dropped`, `close-failed`, or `refused` by the script's `connections: ["refuse", …]`), which makes a connection an owner left open visible. `next("disconnect", …)` scripts the owner's native teardown: `Fail` makes it fail, so the receipt reports `connection: "failed"` with a `disconnect` issue, and `Hold` parks it at a gate however long cleanup waits. A navigation stop is recorded in `calls` as `navigate-stop`, and its arms apply before or after it is sent. Closing the exact affected page after an unknown outcome is recorded as `close-page`; arming `close-page` with `Fail` makes that close fail, so the owner fences. The rest of `browser.control`: `next(operation, outcome)` arms what the next admitted call of one operation does. `Fail` with `outcome: "unknown"` dispatches and then fails: acknowledged exact-page closure preserves healthy peers, while failed closure fences the session and refuses every later mutation `Closed` and `undispatched`; `Fail` with `undispatched` or `rejected` never dispatches. `Hold` parks the call at a `Gate` before or after dispatch, so a test can interrupt or time out a call at a known point and then check what the owner made of it. `Disconnect` drops the connection inside the call. `calls` is the recorder: every admitted call with its operation, page, node, `dispatched` and `settled`, and never a filled value, typed text or address; `document.values` and `document.files` hold those separately. `document.replace` swaps the page's document as a navigation would, so retained nodes and pending waits go stale and a capture learns of a new document; `document.update` changes the same document in place, so controls that keep their `id` keep their identity and waits re-evaluate. `capture.emit` hands a frame to a running interval, `invoke` calls a registered binding as a page would, and `disconnect` drops every open connection outside any call. `closeChecked` returns a `ScriptedCleanupResult`, and `onCleanup` receives it, as the concrete owners do.
|
|
836
1166
|
|
|
837
1167
|
Time is the caller's clock. In-flight navigations, waits, holds and callback deadlines run under the context the session was opened in, so under `it.effect` from `@effect/vitest` a `TestClock.adjust` advances them and nothing real elapses, while `it.live` runs them in real time. `Testing.binding(script)` is the same engine as an opaque `BrowserBinding` for code that constructs a runtime itself; its `browsers` has one control handle for each address connected to, in first-connection order; `Testing.jpeg()` is a small valid JPEG for frames.
|
|
838
1168
|
|
|
@@ -854,6 +1184,10 @@ The native driver, action permits, mutable capture leases and registry lookup ar
|
|
|
854
1184
|
| ------------------------ | ------------------------------------------------------------------------------------- |
|
|
855
1185
|
| `browser` | Common session, bound target, navigation operation and host admission types |
|
|
856
1186
|
| `browser-data`, `errors` | Credential-free schemas and expected browser/initialization errors |
|
|
1187
|
+
| `plan` | Live plan construction, durable encoding, recording and typed run failures |
|
|
1188
|
+
| `plan-data` | Bounded actions, descriptors, conditions and durable plan schemas |
|
|
1189
|
+
| `timeline` | Live evidence capability, explicit JSON codecs and sanitized lifecycle projections |
|
|
1190
|
+
| `timeline-data` | Bounded evidence, cursors, clock offsets, snapshots and typed gap/resource errors |
|
|
857
1191
|
| `bootstrap` | Typed bindings, init/permission plans, readiness and host diagnostics |
|
|
858
1192
|
| `capture` | Bounded live frame intervals, snapshots and final accounting; reexports frame schemas |
|
|
859
1193
|
| `capture-data` | Capture options, binary frame and result schemas without session operations |
|