@effect-agent/platform-cloudflare 0.1.0-beta.110 → 0.1.0-beta.112

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.
@@ -1,9 +1,9 @@
1
- import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted, Ref, Schema, Scope, Semaphore, Stream } from "effect";
1
+ import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted, Schema, Scope, Semaphore, Stream } from "effect";
2
2
  import { PageScreenshotResult } from "effect-agent/page-screenshot";
3
3
  import { SandboxImplementation } from "effect-agent/sandbox";
4
4
  import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
5
5
  import puppeteer from "@cloudflare/puppeteer";
6
- import { BrowserActionResult, BrowserFileSelectionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserSelectFileRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
6
+ import { BrowserActionResult, BrowserExpectedTargetState, BrowserFileSelectionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserSelectFileRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
7
7
  //#region src/internal/browser-failure.ts
8
8
  /** Source-authored stages and classifications only; never retain SDK text or session capabilities. */
9
9
  var BrowserRunFailure = class extends Data.TaggedError("BrowserRunFailure") {
@@ -328,22 +328,19 @@ const ActionObservation = Schema.Struct({
328
328
  before: ActionTargetState,
329
329
  after: Schema.optionalKey(ActionTargetState),
330
330
  afterUnavailable: Schema.Boolean,
331
- network: ActionNetworkState
331
+ network: ActionNetworkState,
332
+ inputMillis: Schema.optionalKey(Schema.Finite),
333
+ observationMillis: Schema.optionalKey(Schema.Finite)
332
334
  });
333
- const PageObservation = Schema.fromJsonString(Schema.Struct({
335
+ const BrowserRunPageObservation = Schema.fromJsonString(Schema.Struct({
336
+ documentId: Schema.NonEmptyString,
334
337
  pageText: BoundedRemoteText,
335
338
  selectorMatchCount: Schema.Natural,
336
339
  controlsTruncated: Schema.Boolean,
337
340
  controls: Schema.Array(Schema.Struct({
341
+ nodeId: Schema.NonEmptyString,
338
342
  selector: BoundedRemoteText,
339
- kind: BoundedRemoteText,
340
- label: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(200))),
341
- checked: Schema.optionalKey(Schema.Boolean),
342
- selected: Schema.optionalKey(Schema.Boolean),
343
- disabled: Schema.optionalKey(Schema.Boolean),
344
- required: Schema.optionalKey(Schema.Boolean),
345
- valid: Schema.optionalKey(Schema.Boolean),
346
- formValid: Schema.optionalKey(Schema.Boolean)
343
+ ...BrowserExpectedTargetState.fields
347
344
  })).check(Schema.isMaxLength(MAX_OBSERVED_CONTROLS))
348
345
  }));
349
346
  const PngBytes = Schema.Uint8Array.check(Schema.isMaxLength(MAX_SCREENSHOT_BYTES), Schema.makeFilter((bytes) => bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10, { title: "PNG bytes" }));
@@ -421,6 +418,24 @@ var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Co
421
418
  })).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
422
419
  }
423
420
  };
421
+ /** Private target identity; never include this checkpoint in model-visible records. */
422
+ var BrowserRunPageIdentity = class extends Schema.Class("BrowserRunPageIdentity")({
423
+ contextId: BrowserRunSessionId,
424
+ targetId: BrowserRunSessionId
425
+ }) {};
426
+ /** Host-owned recovery data. Persist with the application's input receipts and ownership fence. */
427
+ var BrowserRunInteractiveCheckpoint = class extends Schema.Class("BrowserRunInteractiveCheckpoint")({
428
+ sessionId: Schema.RedactedFromValue(BrowserRunSessionId),
429
+ page: BrowserRunPageIdentity,
430
+ policy: InteractiveBrowserPolicy,
431
+ startedAt: Schema.Natural,
432
+ consumedActions: Schema.Natural.check(Schema.isLessThanOrEqualTo(1e3)),
433
+ inputState: Schema.Literals([
434
+ "idle",
435
+ "running",
436
+ "unknown"
437
+ ])
438
+ }) {};
424
439
  /** Cloudflare host authority kept separate from the provider-neutral browser handle. */
425
440
  var BrowserRunInteractiveHost = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveHost") {};
426
441
  const makeProductionRequest = (request) => ({
@@ -594,7 +609,186 @@ const makeActionRequestTracker = (page, signal) => {
594
609
  const disposeActionHandles = async (handles) => {
595
610
  await boundedBestEffort(Promise.allSettled(handles.map(async (handle) => handle.dispose())), ACTION_POST_STATE_MILLIS);
596
611
  };
597
- const runObservedPageAction = async (page, selector, signal, onDispatch, action, validate) => {
612
+ /** Runs entirely in the page realm for both observations and the last bound-element guard. */
613
+ const observePage = (target, requestedSelector, maximum, maximumControls, input) => {
614
+ const pageDocument = Reflect.get(globalThis, "document");
615
+ const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
616
+ const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
617
+ const element = target ?? (matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0));
618
+ if (target !== void 0 && Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
619
+ if (element === null) return { _tag: "MissingElement" };
620
+ if (element === void 0) return { _tag: "MissingElement" };
621
+ const innerText = Reflect.get(element, "innerText");
622
+ const textContent = Reflect.get(element, "textContent");
623
+ const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
624
+ const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
625
+ const optionSelector = "select option";
626
+ const secondaryControlSelector = "a[href]";
627
+ const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
628
+ const selectorFor = (candidate) => {
629
+ const parts = [];
630
+ let current = candidate;
631
+ while (current !== null && current !== void 0) {
632
+ const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
633
+ if (tagName === "") break;
634
+ const parent = Reflect.get(current, "parentElement");
635
+ if (parent === null) {
636
+ parts.push(tagName);
637
+ break;
638
+ }
639
+ let sibling = Reflect.get(current, "previousElementSibling");
640
+ let index = 1;
641
+ while (sibling !== null && sibling !== void 0) {
642
+ if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
643
+ sibling = Reflect.get(sibling, "previousElementSibling");
644
+ }
645
+ parts.push(`${tagName}:nth-of-type(${index})`);
646
+ current = parent;
647
+ }
648
+ return parts.reverse().join(" > ");
649
+ };
650
+ const visible = (candidate) => {
651
+ const hidden = Reflect.get(candidate, "hidden");
652
+ const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
653
+ const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
654
+ return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
655
+ };
656
+ const candidates = [];
657
+ let controlsTruncated = false;
658
+ const consider = (candidate) => {
659
+ const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
660
+ const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
661
+ if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
662
+ candidates.push(candidate);
663
+ if (candidates.length > maximumControls) controlsTruncated = true;
664
+ };
665
+ const elementMatches = Reflect.get(element, "matches");
666
+ if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
667
+ const considerSelector = (candidateSelector) => {
668
+ const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
669
+ if (typeof descendants !== "object" || descendants === null) return;
670
+ const descendantCount = Reflect.get(descendants, "length");
671
+ for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
672
+ };
673
+ considerSelector(primaryControlSelector);
674
+ if (!controlsTruncated) considerSelector(optionSelector);
675
+ if (!controlsTruncated) considerSelector(secondaryControlSelector);
676
+ const identityKey = Symbol.for("@effect-agent/browser-observation");
677
+ let identity = Reflect.get(globalThis, identityKey);
678
+ if (identity === void 0 || identity.document !== pageDocument) {
679
+ identity = {
680
+ document: pageDocument,
681
+ documentId: crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-"),
682
+ nodes: /* @__PURE__ */ new WeakMap()
683
+ };
684
+ Reflect.set(globalThis, identityKey, identity);
685
+ }
686
+ const controls = candidates.slice(0, maximumControls).map((candidate) => {
687
+ let nodeId = identity.nodes.get(candidate);
688
+ if (nodeId === void 0) {
689
+ nodeId = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-");
690
+ identity.nodes.set(candidate, nodeId);
691
+ }
692
+ const associated = Reflect.get(candidate, "control") ?? candidate;
693
+ const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
694
+ const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
695
+ const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
696
+ const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
697
+ const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
698
+ const associatedLabels = Reflect.get(associated, "labels");
699
+ const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
700
+ const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
701
+ const checked = Reflect.get(associated, "checked");
702
+ const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
703
+ const disabled = Reflect.get(associated, "disabled");
704
+ const required = Reflect.get(associated, "required");
705
+ const validity = Reflect.get(associated, "validity");
706
+ const form = Reflect.get(associated, "form");
707
+ const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
708
+ const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
709
+ const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
710
+ const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
711
+ return {
712
+ nodeId,
713
+ ...inputType === "" ? {} : { inputType: inputType.slice(0, 64) },
714
+ selector: selectorFor(candidate),
715
+ kind: (tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName).slice(0, 128),
716
+ ...label === "" ? {} : { label },
717
+ ...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
718
+ ...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
719
+ ...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
720
+ ...typeof required === "boolean" ? { required } : {},
721
+ ...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
722
+ ...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
723
+ };
724
+ });
725
+ if (input !== void 0) {
726
+ const expected = input.expectedTarget;
727
+ const control = controls.find((candidate) => candidate.nodeId === expected.nodeId);
728
+ if (target === void 0 || identity.documentId !== expected.documentId || identity.nodes.get(target) !== expected.nodeId || control === void 0 || expected.state !== void 0 && [
729
+ "kind",
730
+ "inputType",
731
+ "label",
732
+ "checked",
733
+ "selected",
734
+ "disabled",
735
+ "required",
736
+ "valid",
737
+ "formValid"
738
+ ].some((field) => control[field] !== expected.state?.[field]) || Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
739
+ if (expected.scopeSelector !== void 0) {
740
+ let roots;
741
+ try {
742
+ roots = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [expected.scopeSelector]);
743
+ } catch {
744
+ return { _tag: "MissingElement" };
745
+ }
746
+ if (typeof roots !== "object" || roots === null) return { _tag: "MissingElement" };
747
+ const root = Reflect.get(roots, 0);
748
+ if (Reflect.get(roots, "length") !== 1 || root === void 0 || !Reflect.apply(Reflect.get(root, "contains"), root, [target])) return { _tag: "MissingElement" };
749
+ }
750
+ if (input.mode === "click") {
751
+ const prototype = Reflect.get(Reflect.get(globalThis, "HTMLElement"), "prototype");
752
+ Reflect.apply(Reflect.get(prototype, "click"), target, []);
753
+ } else {
754
+ let prototype = Reflect.getPrototypeOf(target);
755
+ let setValue;
756
+ while (prototype !== null) {
757
+ const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
758
+ if (typeof setter === "function") {
759
+ setValue = setter;
760
+ break;
761
+ }
762
+ prototype = Reflect.getPrototypeOf(prototype);
763
+ }
764
+ if (setValue === void 0 || input.value === void 0) return { _tag: "MissingElement" };
765
+ Reflect.apply(setValue, target, [input.value]);
766
+ const dispatchEvent = Reflect.get(target, "dispatchEvent");
767
+ Reflect.apply(dispatchEvent, target, [new Event("input", { bubbles: true })]);
768
+ Reflect.apply(dispatchEvent, target, [new Event("change", { bubbles: true })]);
769
+ }
770
+ return {
771
+ _tag: "Text",
772
+ text: ""
773
+ };
774
+ }
775
+ const text = JSON.stringify({
776
+ documentId: identity.documentId,
777
+ pageText,
778
+ selectorMatchCount,
779
+ controls,
780
+ controlsTruncated
781
+ });
782
+ const observed = new TextEncoder().encode(text).byteLength;
783
+ return observed > maximum ? {
784
+ _tag: "OverLimit",
785
+ observed
786
+ } : {
787
+ _tag: "Text",
788
+ text
789
+ };
790
+ };
791
+ const runObservedPageAction = async (page, selector, signal, onDispatch, action, validate, onComplete) => {
598
792
  if (signal.aborted) throw new BrowserRunActionUndispatched(0);
599
793
  const before = Schema.decodeUnknownSync(ActionTargetState)(await readActionTarget(page, selector));
600
794
  if (before.matchCount !== 1 || before.invalidSelector === true) throw new BrowserRunActionUndispatched(before.matchCount);
@@ -619,12 +813,17 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action,
619
813
  tracker = makeActionRequestTracker(page, signal);
620
814
  if (signal.aborted) throw new BrowserRunActionUndispatched(1);
621
815
  onDispatch();
816
+ const inputStarted = performance.now();
622
817
  await action(matches[0]);
818
+ const inputFinished = performance.now();
819
+ onComplete?.();
623
820
  tracker.markActionSettled();
624
821
  const network = await tracker.wait();
625
822
  const after = signal.aborted ? void 0 : await readActionTargetAfter(page, selector);
626
823
  return {
627
824
  before,
825
+ inputMillis: Math.max(0, inputFinished - inputStarted),
826
+ observationMillis: Math.max(0, performance.now() - inputFinished),
628
827
  ...after === void 0 ? {} : { after },
629
828
  afterUnavailable: after === void 0,
630
829
  network
@@ -635,10 +834,29 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action,
635
834
  await dispose();
636
835
  }
637
836
  };
837
+ const guardedPageInput = async (element, expectedTarget, mode, value) => {
838
+ if (Schema.decodeUnknownSync(TextObservation)(await element.evaluate(observePage, void 0, 262144, 1, {
839
+ mode,
840
+ expectedTarget,
841
+ ...value === void 0 ? {} : { value }
842
+ }))._tag !== "Text") throw new BrowserRunActionUndispatched(1);
843
+ };
638
844
  const makeProductionPage = (page) => {
639
845
  const prepareFileSelection = makeFileSelection(page);
640
846
  const listeners = /* @__PURE__ */ new Map();
641
847
  return {
848
+ identity: async () => {
849
+ const cdp = await page.createCDPSession();
850
+ try {
851
+ const { targetInfo } = await cdp.send("Target.getTargetInfo");
852
+ return Schema.decodeUnknownSync(BrowserRunPageIdentity)({
853
+ contextId: targetInfo.browserContextId,
854
+ targetId: targetInfo.targetId
855
+ });
856
+ } finally {
857
+ await cdp.detach();
858
+ }
859
+ },
642
860
  close: async () => {
643
861
  if (!page.browser().isConnected()) return;
644
862
  try {
@@ -669,119 +887,11 @@ const makeProductionPage = (page) => {
669
887
  },
670
888
  url: () => page.url(),
671
889
  readText: async (selector, maximumBytes) => {
672
- const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate((requestedSelector, maximum, maximumControls) => {
673
- const pageDocument = Reflect.get(globalThis, "document");
674
- const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
675
- const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
676
- const element = matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0);
677
- if (element === null) return { _tag: "MissingElement" };
678
- if (element === void 0) return { _tag: "MissingElement" };
679
- const innerText = Reflect.get(element, "innerText");
680
- const textContent = Reflect.get(element, "textContent");
681
- const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
682
- const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
683
- const optionSelector = "select option";
684
- const secondaryControlSelector = "a[href]";
685
- const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
686
- const selectorFor = (candidate) => {
687
- const parts = [];
688
- let current = candidate;
689
- while (current !== null && current !== void 0) {
690
- const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
691
- if (tagName === "") break;
692
- const parent = Reflect.get(current, "parentElement");
693
- if (parent === null) {
694
- parts.push(tagName);
695
- break;
696
- }
697
- let sibling = Reflect.get(current, "previousElementSibling");
698
- let index = 1;
699
- while (sibling !== null && sibling !== void 0) {
700
- if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
701
- sibling = Reflect.get(sibling, "previousElementSibling");
702
- }
703
- parts.push(`${tagName}:nth-of-type(${index})`);
704
- current = parent;
705
- }
706
- return parts.reverse().join(" > ");
707
- };
708
- const visible = (candidate) => {
709
- const hidden = Reflect.get(candidate, "hidden");
710
- const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
711
- const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
712
- return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
713
- };
714
- const candidates = [];
715
- let controlsTruncated = false;
716
- const consider = (candidate) => {
717
- const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
718
- const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
719
- if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
720
- candidates.push(candidate);
721
- if (candidates.length > maximumControls) controlsTruncated = true;
722
- };
723
- const elementMatches = Reflect.get(element, "matches");
724
- if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
725
- const considerSelector = (candidateSelector) => {
726
- const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
727
- if (typeof descendants !== "object" || descendants === null) return;
728
- const descendantCount = Reflect.get(descendants, "length");
729
- for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
730
- };
731
- considerSelector(primaryControlSelector);
732
- if (!controlsTruncated) considerSelector(optionSelector);
733
- if (!controlsTruncated) considerSelector(secondaryControlSelector);
734
- const controls = candidates.slice(0, maximumControls).map((candidate) => {
735
- const associated = Reflect.get(candidate, "control") ?? candidate;
736
- const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
737
- const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
738
- const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
739
- const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
740
- const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
741
- const associatedLabels = Reflect.get(associated, "labels");
742
- const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
743
- const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
744
- const checked = Reflect.get(associated, "checked");
745
- const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
746
- const disabled = Reflect.get(associated, "disabled");
747
- const required = Reflect.get(associated, "required");
748
- const validity = Reflect.get(associated, "validity");
749
- const form = Reflect.get(associated, "form");
750
- const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
751
- const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
752
- const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
753
- const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
754
- return {
755
- selector: selectorFor(candidate),
756
- kind: tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName,
757
- ...label === "" ? {} : { label },
758
- ...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
759
- ...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
760
- ...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
761
- ...typeof required === "boolean" ? { required } : {},
762
- ...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
763
- ...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
764
- };
765
- });
766
- const text = JSON.stringify({
767
- pageText,
768
- selectorMatchCount,
769
- controls,
770
- controlsTruncated
771
- });
772
- const observed = new TextEncoder().encode(text).byteLength;
773
- return observed > maximum ? {
774
- _tag: "OverLimit",
775
- observed
776
- } : {
777
- _tag: "Text",
778
- text
779
- };
780
- }, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
781
- if (observation._tag === "Text") Schema.decodeSync(PageObservation)(observation.text);
890
+ const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate(observePage, void 0, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
891
+ if (observation._tag === "Text") Schema.decodeSync(BrowserRunPageObservation)(observation.text);
782
892
  return observation;
783
893
  },
784
- fill: (selector, value, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.evaluate((element, nextValue) => {
894
+ fill: (selector, value, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget !== void 0 ? guardedPageInput(element, expectedTarget, "fill", value) : element.evaluate((element, nextValue) => {
785
895
  let prototype = Reflect.getPrototypeOf(element);
786
896
  let setValue;
787
897
  while (prototype !== null) {
@@ -801,12 +911,12 @@ const makeProductionPage = (page) => {
801
911
  Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
802
912
  Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
803
913
  }
804
- }, value)),
805
- click: (selector, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.click()),
806
- selectFile: async (request, signal, onDispatch) => {
914
+ }, value), void 0, onComplete),
915
+ click: (selector, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget === void 0 ? element.click() : guardedPageInput(element, expectedTarget, "click"), void 0, onComplete),
916
+ selectFile: async (request, signal, onDispatch, onComplete) => {
807
917
  const selection = await prepareFileSelection(request, signal);
808
918
  try {
809
- return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate);
919
+ return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate, onComplete);
810
920
  } finally {
811
921
  await selection.close();
812
922
  }
@@ -843,6 +953,20 @@ const makeProductionContext = (context, viewport) => ({
843
953
  }
844
954
  });
845
955
  const makeProductionBrowser = (browser, viewport) => ({
956
+ detach: () => browser.disconnect(),
957
+ reattach: async (identity) => {
958
+ const context = browser.browserContexts().find((candidate) => candidate.id === identity.contextId);
959
+ if (context === void 0) return void 0;
960
+ const pages = await context.pages();
961
+ if (pages.length > 64) throw new Error("The browser target inventory exceeds its bound");
962
+ for (const candidate of pages) {
963
+ const page = makeProductionPage(candidate);
964
+ if ((await page.identity?.())?.targetId === identity.targetId) return {
965
+ context: makeProductionContext(context),
966
+ page
967
+ };
968
+ }
969
+ },
846
970
  createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
847
971
  close: async () => {
848
972
  if (!browser.isConnected()) return;
@@ -866,26 +990,42 @@ const protocolError = (message, cause) => InteractiveBrowserProtocolError.make({
866
990
  message,
867
991
  ...cause === void 0 ? {} : { cause }
868
992
  });
869
- const actionError = (operation, cause) => InteractiveBrowserActionError.make({
993
+ const failReportedProtocol = (operation, error, evidence = error.evidence) => reportBrowserCause(operation, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(InteractiveBrowserProtocolError.make({
994
+ implementation: browserRunInteractiveImplementation,
995
+ message: error.message,
996
+ ...evidence === void 0 ? {} : { evidence }
997
+ })))));
998
+ const actionError = (operation, cause, evidence) => InteractiveBrowserActionError.make({
870
999
  implementation: browserRunInteractiveImplementation,
871
1000
  operation,
1001
+ ...evidence === void 0 ? {} : { evidence },
872
1002
  message: `The interactive browser ${operation} operation failed`,
873
1003
  ...cause === void 0 ? {} : { cause }
874
1004
  });
875
1005
  const undispatchedActionError = (operation) => InteractiveBrowserActionError.make({
876
1006
  implementation: browserRunInteractiveImplementation,
877
1007
  operation,
878
- message: `The interactive browser ${operation} operation was not dispatched`
1008
+ message: `The interactive browser ${operation} operation was not dispatched`,
1009
+ evidence: {
1010
+ stage: "preparation",
1011
+ dispatch: "not-dispatched",
1012
+ session: "attached"
1013
+ }
879
1014
  });
880
1015
  /** Recognizes a local pre-dispatch refusal without exposing selector or page content. */
881
- const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity && (error.operation === "click" || error.operation === "fill") && error.message === `The interactive browser ${error.operation} operation was not dispatched`;
1016
+ const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity && error.evidence?.dispatch === "not-dispatched";
882
1017
  const policyError = (message) => InteractiveBrowserPolicyDeniedError.make({
883
1018
  implementation: browserRunInteractiveImplementation,
884
1019
  message
885
1020
  });
886
- const expiredError = () => InteractiveBrowserExpiredError.make({
1021
+ const expiredError = (session = "lost") => InteractiveBrowserExpiredError.make({
887
1022
  implementation: browserRunInteractiveImplementation,
888
- message: "The remote browser is no longer usable"
1023
+ message: "The remote browser is no longer usable",
1024
+ evidence: {
1025
+ stage: "preparation",
1026
+ dispatch: "not-dispatched",
1027
+ session
1028
+ }
889
1029
  });
890
1030
  const causeText = (cause) => {
891
1031
  if (cause instanceof Error) return cause.message.slice(0, 8e3);
@@ -893,6 +1033,7 @@ const causeText = (cause) => {
893
1033
  };
894
1034
  const isCapacityRefusal = (cause) => cause instanceof BrowserRunFailure && cause.status === 429;
895
1035
  const isRemoteClosure = (cause) => /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(causeText(cause));
1036
+ const inputMayStillRun = (cause) => isRemoteClosure(cause) || cause instanceof Error && cause.name === "TimeoutError" || cause instanceof BrowserRunFailure && cause.reason === "timeout";
896
1037
  const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (input) {
897
1038
  const decoded = yield* Schema.decodeEffect(InteractiveBrowserPolicy, { onExcessProperty: "error" })(input).pipe(Effect.mapError(() => policyError("The interactive browser policy is malformed")));
898
1039
  if (decoded.network._tag === "PublicWeb") return yield* InteractiveBrowserUnsupportedError.make({
@@ -956,9 +1097,28 @@ const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(functio
956
1097
  orElse: () => Effect.sync(() => onTimeout?.()).pipe(Effect.andThen(deadlineError(policy, startedAt)))
957
1098
  }));
958
1099
  });
1100
+ const trackPendingInput = (state, pending) => {
1101
+ state.pendingInputs.add(pending);
1102
+ pending.then(() => state.pendingInputs.delete(pending), (cause) => {
1103
+ state.pendingInputs.delete(pending);
1104
+ if (state.disconnected.value || inputMayStillRun(cause)) state.uncertain.value = true;
1105
+ if (isRemoteClosure(cause)) state.disconnected.value = true;
1106
+ });
1107
+ return pending;
1108
+ };
1109
+ const makeHostInputObserver = Effect.fnUntraced(function* (state, operation) {
1110
+ const reporterContext = yield* Effect.context();
1111
+ return (pending, signal) => {
1112
+ pending.then(void 0, (cause) => {
1113
+ if (signal.aborted) return Effect.runPromiseWith(reporterContext)(reportBrowserCause(operation, Cause.fail(cause)));
1114
+ });
1115
+ return trackPendingInput(state, pending);
1116
+ };
1117
+ });
959
1118
  const stateFailure = (state) => {
960
1119
  if (state.violation.value !== void 0) return state.violation.value;
961
- if (state.closed.value || state.disconnected.value || state.uncertain.value) return expiredError();
1120
+ if (state.closed.value) return expiredError();
1121
+ if (state.disconnected.value) return expiredError("disconnected");
962
1122
  };
963
1123
  var BrowserRunRemoteFailure = class {
964
1124
  cause;
@@ -1012,46 +1172,92 @@ const makeRequestListener = (policy, state) => (request) => {
1012
1172
  };
1013
1173
  const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state, close) {
1014
1174
  const permits = yield* Semaphore.make(1);
1015
- const actions = yield* Ref.make(0);
1016
- const remote = (operation, evaluate) => Effect.tryPromise({
1017
- try: evaluate,
1018
- catch: (cause) => new BrowserRunRemoteFailure(cause)
1019
- }).pipe(Effect.catch((failure) => {
1020
- const cause = failure.cause;
1021
- if (cause instanceof BrowserRunActionUndispatched) return Effect.logInfo("Browser interactive action was not dispatched").pipe(Effect.annotateLogs({
1022
- "browser.action": operation,
1023
- "browser.selector_match_count": cause.matchCount
1024
- }), Effect.andThen(Effect.fail(undispatchedActionError(operation))));
1025
- if (state.disconnected.value || isRemoteClosure(cause)) {
1026
- state.disconnected.value = true;
1027
- return Effect.fail(expiredError());
1028
- }
1029
- return Effect.fail(actionError(operation, cause));
1030
- }));
1031
- const observedAction = (operation, evaluate) => Effect.suspend(() => {
1032
- let dispatched = false;
1033
- let pending;
1034
- return remote(operation, (signal) => {
1035
- pending = evaluate(signal, () => {
1036
- dispatched = true;
1037
- });
1038
- return pending;
1039
- }).pipe(Effect.onInterrupt(() => Effect.gen(function* () {
1040
- state.uncertain.value = true;
1041
- yield* Effect.logWarning("Browser interactive action interrupted").pipe(Effect.annotateLogs({
1175
+ let currentOperation;
1176
+ let generation = 0;
1177
+ let evidence = {
1178
+ stage: "preparation",
1179
+ dispatch: "not-dispatched",
1180
+ session: "attached"
1181
+ };
1182
+ const reporterContext = yield* Effect.context();
1183
+ const remote = (operation, evaluate, observed = false) => Effect.suspend(() => {
1184
+ currentOperation = operation;
1185
+ const mutating = operation !== "read-text" && operation !== "screenshot";
1186
+ const currentGeneration = ++generation;
1187
+ evidence = {
1188
+ stage: observed ? "preparation" : mutating ? "input" : "observation",
1189
+ dispatch: observed || !mutating ? "not-dispatched" : "running",
1190
+ session: "attached"
1191
+ };
1192
+ return Effect.tryPromise({
1193
+ try: (signal) => {
1194
+ const pending = evaluate(signal);
1195
+ if (mutating) trackPendingInput(state, pending);
1196
+ pending.then(() => {
1197
+ if (mutating && generation === currentGeneration) evidence = {
1198
+ ...evidence,
1199
+ stage: "observation",
1200
+ dispatch: "completed"
1201
+ };
1202
+ }, (cause) => {
1203
+ if (state.disconnected.value || isRemoteClosure(cause)) {
1204
+ state.disconnected.value = true;
1205
+ if (mutating) state.uncertain.value = true;
1206
+ }
1207
+ if (generation === currentGeneration && evidence.dispatch === "running") evidence = {
1208
+ ...evidence,
1209
+ dispatch: "unknown"
1210
+ };
1211
+ if (signal.aborted && !(cause instanceof BrowserRunActionUndispatched)) Effect.runPromiseWith(reporterContext)(reportBrowserCause("interactive.input.late", Cause.fail(cause)));
1212
+ });
1213
+ return pending;
1214
+ },
1215
+ catch: (cause) => new BrowserRunRemoteFailure(cause)
1216
+ }).pipe(Effect.catch((failure) => {
1217
+ if (failure.cause instanceof BrowserRunActionUndispatched) return Effect.logInfo("Browser interactive action was not dispatched").pipe(Effect.annotateLogs({
1042
1218
  "browser.action": operation,
1043
- "browser.action_dispatched": dispatched,
1044
- "browser.action_outcome_unknown": dispatched
1045
- }));
1046
- const completion = pending;
1047
- if (completion !== void 0) yield* Effect.promise(() => boundedBestEffort(completion, 500));
1048
- })));
1219
+ "browser.selector_match_count": failure.cause.matchCount
1220
+ }), Effect.andThen(Effect.fail(undispatchedActionError(operation))));
1221
+ if (mutating && inputMayStillRun(failure.cause)) state.uncertain.value = true;
1222
+ if (isRemoteClosure(failure.cause)) {
1223
+ state.disconnected.value = true;
1224
+ if (mutating) state.uncertain.value = true;
1225
+ }
1226
+ evidence = {
1227
+ ...evidence,
1228
+ dispatch: evidence.dispatch === "running" ? "unknown" : evidence.dispatch,
1229
+ session: state.disconnected.value ? "disconnected" : "attached"
1230
+ };
1231
+ return reportBrowserCause(`interactive.${operation}.${evidence.stage}.${evidence.dispatch}`, Cause.fail(failure.cause)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError(operation, void 0, evidence)))));
1232
+ }));
1049
1233
  });
1234
+ const observedAction = (operation, evaluate) => remote(operation, (signal) => {
1235
+ const observedGeneration = generation;
1236
+ return evaluate(signal, () => {
1237
+ if (generation === observedGeneration) evidence = {
1238
+ ...evidence,
1239
+ stage: "input",
1240
+ dispatch: "running"
1241
+ };
1242
+ }, () => {
1243
+ if (generation === observedGeneration) evidence = {
1244
+ ...evidence,
1245
+ stage: "observation",
1246
+ dispatch: "completed"
1247
+ };
1248
+ });
1249
+ }, true).pipe(Effect.onInterrupt(() => Effect.logWarning("Browser interactive action interrupted").pipe(Effect.annotateLogs({
1250
+ "browser.action": operation,
1251
+ "browser.action_dispatched": evidence.dispatch !== "not-dispatched",
1252
+ "browser.action_outcome_unknown": evidence.dispatch === "running" || evidence.dispatch === "unknown"
1253
+ }))));
1050
1254
  const decodeActionObservation = Effect.fn("BrowserRunInteractive.decodeActionObservation")(function* (raw) {
1051
1255
  return yield* Schema.decodeUnknownEffect(ActionObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed action observation", cause)));
1052
1256
  });
1053
1257
  const logActionObservation = (operation, observation) => Effect.logInfo("Browser interactive action observed").pipe(Effect.annotateLogs({
1054
1258
  "browser.action": operation,
1259
+ "browser.input_millis": observation.inputMillis,
1260
+ "browser.observation_millis": observation.observationMillis,
1055
1261
  "browser.selector_match_count": observation.before.matchCount,
1056
1262
  ...observation.before.kind === void 0 ? {} : { "browser.target_kind": observation.before.kind },
1057
1263
  ...observation.before.checked === void 0 ? {} : { "browser.target_checked_before": observation.before.checked },
@@ -1076,18 +1282,38 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
1076
1282
  "browser.fetch_xhr_pending": observation.network.pending,
1077
1283
  "browser.network_settle_timed_out": observation.network.settleTimedOut
1078
1284
  }));
1079
- const run = (effect, preflight = Effect.void, consumeAction = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
1285
+ const run = (effect, preflight = Effect.void, consumeAction = true, mutating = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
1080
1286
  const unavailable = stateFailure(state);
1081
- if (unavailable !== void 0) return yield* unavailable;
1287
+ if (unavailable !== void 0) {
1288
+ if (unavailable._tag === "InteractiveBrowserProtocolError") {
1289
+ yield* reportBrowserCause("interactive.policy", Cause.fail(unavailable));
1290
+ const safe = reportedBrowserError(InteractiveBrowserProtocolError.make({
1291
+ implementation: browserRunInteractiveImplementation,
1292
+ message: unavailable.message,
1293
+ evidence: unavailable.evidence ?? {
1294
+ stage: "preparation",
1295
+ dispatch: "not-dispatched",
1296
+ session: "attached"
1297
+ }
1298
+ }));
1299
+ if (state.violation.value === unavailable) state.violation.value = safe;
1300
+ return yield* safe;
1301
+ }
1302
+ return yield* unavailable;
1303
+ }
1304
+ if (mutating && (state.uncertain.value || state.pendingInputs.size > 0)) return yield* InteractiveBrowserBusyError.make({
1305
+ implementation: browserRunInteractiveImplementation,
1306
+ message: "Earlier browser input has not been proven stopped"
1307
+ });
1308
+ currentOperation = void 0;
1309
+ generation++;
1082
1310
  yield* preflight;
1083
1311
  if (consumeAction) {
1084
- const admitted = yield* Ref.modify(actions, (count) => count >= policy.maxActions ? [{
1085
- allowed: false,
1086
- observed: count + 1
1087
- }, count] : [{
1088
- allowed: true,
1089
- observed: count + 1
1090
- }, count + 1]);
1312
+ const admitted = {
1313
+ allowed: state.actions.value < policy.maxActions,
1314
+ observed: state.actions.value + 1
1315
+ };
1316
+ if (admitted.allowed) state.actions.value++;
1091
1317
  if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
1092
1318
  implementation: browserRunInteractiveImplementation,
1093
1319
  limit: "actions",
@@ -1096,22 +1322,29 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
1096
1322
  message: "The browser action limit was reached"
1097
1323
  });
1098
1324
  }
1099
- const completed = effect.pipe(Effect.catch((error) => {
1100
- const failure = stateFailure(state);
1101
- return Effect.fail(failure ?? error);
1102
- }), Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.flatMap(() => {
1325
+ const completed = effect.pipe(Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.andThen(Effect.suspend(() => {
1103
1326
  const failure = stateFailure(state);
1104
1327
  return failure === void 0 ? Effect.succeed(result) : Effect.fail(failure);
1105
- }))));
1106
- return yield* withinDeadline(completed, policy, startedAt, () => {
1107
- state.uncertain.value = true;
1108
- }).pipe(Effect.onInterrupt(() => Effect.sync(() => {
1109
- state.uncertain.value = true;
1110
- })), Effect.catch((error) => {
1111
- if (Schema.is(InteractiveBrowserLimitError)(error) && error.limit === "elapsed") return Effect.fail(error);
1112
- const failure = stateFailure(state);
1113
- if (Schema.is(InteractiveBrowserActionError)(error) && !isBrowserRunUndispatchedActionError(error)) state.uncertain.value = true;
1114
- return Effect.fail(failure ?? error);
1328
+ })))));
1329
+ return yield* withinDeadline(completed, policy, startedAt).pipe(Effect.mapError((error) => currentOperation === void 0 ? error : error._tag === "InteractiveBrowserLimitError" ? InteractiveBrowserLimitError.make({
1330
+ implementation: error.implementation,
1331
+ limit: error.limit,
1332
+ maximum: error.maximum,
1333
+ observed: error.observed,
1334
+ message: error.message,
1335
+ evidence
1336
+ }) : error._tag === "InteractiveBrowserPolicyDeniedError" ? InteractiveBrowserPolicyDeniedError.make({
1337
+ implementation: error.implementation,
1338
+ message: error.message,
1339
+ evidence
1340
+ }) : error), Effect.catchIf((error) => currentOperation !== void 0 && error._tag !== "InteractiveBrowserActionError" && error._tag !== "InteractiveBrowserLimitError" && error._tag !== "InteractiveBrowserPolicyDeniedError" && error._tag !== "InteractiveBrowserExpiredError", (error) => {
1341
+ const safe = reportedBrowserError(error._tag === "InteractiveBrowserProtocolError" ? InteractiveBrowserProtocolError.make({
1342
+ implementation: browserRunInteractiveImplementation,
1343
+ message: error.message,
1344
+ evidence
1345
+ }) : actionError(currentOperation ?? "read-text", void 0, evidence));
1346
+ if (state.violation.value === error) state.violation.value = safe;
1347
+ return reportBrowserCause(`interactive.${currentOperation}.observation`, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(safe)));
1115
1348
  }));
1116
1349
  })).pipe(Effect.flatMap(Effect.fromOption(() => InteractiveBrowserBusyError.make({
1117
1350
  implementation: browserRunInteractiveImplementation,
@@ -1126,7 +1359,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
1126
1359
  readText: (request) => run(Effect.gen(function* () {
1127
1360
  const raw = yield* remote("read-text", () => page.readText(request.selector, policy.maxReturnedBytes));
1128
1361
  const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed text observation", cause)));
1129
- if (observation._tag === "MissingElement") return yield* actionError("read-text");
1362
+ if (observation._tag === "MissingElement") return yield* actionError("read-text", void 0, evidence);
1130
1363
  if (observation._tag === "OverLimit") return yield* InteractiveBrowserLimitError.make({
1131
1364
  implementation: browserRunInteractiveImplementation,
1132
1365
  limit: "returned-bytes",
@@ -1143,19 +1376,19 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
1143
1376
  message: "The browser returned-text limit was reached"
1144
1377
  });
1145
1378
  return yield* Schema.decodeEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
1146
- })),
1379
+ }), Effect.void, true, false),
1147
1380
  fill: (request) => run(Effect.gen(function* () {
1148
- const observation = yield* observedAction("fill", (signal, onDispatch) => page.fill(request.selector, request.value, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
1381
+ const observation = yield* observedAction("fill", (signal, onDispatch, onComplete) => page.fill(request.selector, request.value, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
1149
1382
  yield* logActionObservation("fill", observation);
1150
1383
  return yield* decodeActionResult(page, policy);
1151
1384
  })),
1152
1385
  click: (request) => run(Effect.gen(function* () {
1153
- const observation = yield* observedAction("click", (signal, onDispatch) => page.click(request.selector, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
1386
+ const observation = yield* observedAction("click", (signal, onDispatch, onComplete) => page.click(request.selector, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
1154
1387
  yield* logActionObservation("click", observation);
1155
1388
  return yield* decodeActionResult(page, policy);
1156
1389
  })),
1157
1390
  selectFile: (request) => Schema.decodeEffect(BrowserSelectFileRequest)(request).pipe(Effect.mapError(() => policyError("The browser file selection request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
1158
- const observation = yield* observedAction("select-file", (signal, onDispatch) => page.selectFile(decoded, signal, onDispatch)).pipe(Effect.flatMap(decodeActionObservation));
1391
+ const observation = yield* observedAction("select-file", (signal, onDispatch, onComplete) => page.selectFile(decoded, signal, onDispatch, onComplete)).pipe(Effect.flatMap(decodeActionObservation));
1159
1392
  yield* logActionObservation("select-file", observation);
1160
1393
  const result = yield* decodeActionResult(page, policy);
1161
1394
  return BrowserFileSelectionResult.make({
@@ -1180,7 +1413,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
1180
1413
  mediaType: "image/png",
1181
1414
  bytes: new Uint8Array(bytes)
1182
1415
  }).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
1183
- }), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
1416
+ }), decodeActionResult(page, policy).pipe(Effect.asVoid), true, false))),
1184
1417
  scroll: (request) => Schema.decodeEffect(BrowserScrollRequest)(request).pipe(Effect.mapError(() => policyError("The browser scroll request is malformed")), Effect.flatMap((decoded) => run(remote("scroll", () => page.scroll(decoded.deltaX, decoded.deltaY)).pipe(Effect.andThen(decodeActionResult(page, policy))), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
1185
1418
  close
1186
1419
  },
@@ -1217,6 +1450,7 @@ const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry)
1217
1450
  }))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
1218
1451
  const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
1219
1452
  const cleanupContext = yield* Effect.context();
1453
+ const observeInput = yield* makeHostInputObserver(state, "interactive.control.late");
1220
1454
  const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
1221
1455
  try: (signal) => {
1222
1456
  const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) => Effect.runPromiseWith(cleanupContext)(Effect.tryPromise({
@@ -1231,13 +1465,19 @@ const cdpCommand = (page, state, command, parameters, output, malformedMessage)
1231
1465
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
1232
1466
  }), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
1233
1467
  const raw = yield* Effect.tryPromise({
1234
- try: () => cdp.send(command, parameters),
1468
+ try: (signal) => {
1469
+ const pending = cdp.send(command, parameters);
1470
+ return command === "Cloudflare.getHandoffState" ? pending : observeInput(pending, signal);
1471
+ },
1235
1472
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
1236
1473
  });
1237
1474
  return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.catch((error) => reportBrowserCause("interactive.command.decode", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(protocolError(malformedMessage)))))));
1238
- }));
1475
+ })).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.control", error)));
1239
1476
  const makeHostService = (binding) => {
1240
- const closeSession = binding.closeSession;
1477
+ const owners = /* @__PURE__ */ new Map();
1478
+ const closeSession = (sessionId) => binding.closeSession(sessionId).pipe(Effect.tap(() => Effect.sync(() => {
1479
+ owners.delete(Redacted.value(sessionId));
1480
+ })));
1241
1481
  const terminate = Effect.fn("BrowserRunInteractiveHost.terminate")(function* (sessionId, entries) {
1242
1482
  const deadline = (yield* Clock.currentTimeMillis) + 1e4;
1243
1483
  yield* closeSession(sessionId).pipe(Effect.interruptible, Effect.timeoutOrElse({
@@ -1255,10 +1495,10 @@ const makeHostService = (binding) => {
1255
1495
  }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError((cause) => actionError("close", cause)), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
1256
1496
  yield* terminate(Redacted.make(sessionId), [closeEntry(browser.close, "Closing the local browser connection failed")]);
1257
1497
  });
1258
- const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy) {
1498
+ const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy, resume, pendingInput = false) {
1259
1499
  const scope = yield* Scope.Scope;
1260
1500
  const fixedPolicy = yield* snapshotPolicy(policy);
1261
- const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
1501
+ const startedAt = resume?.startedAt ?? (yield* Clock.currentTimeMillis);
1262
1502
  const runCleanup = (effect) => Effect.runPromiseWith(cleanupContext)(effect.pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause))));
1263
1503
  const cleanupContext = yield* Effect.context();
1264
1504
  const observeLate = (signal, pending) => {
@@ -1270,29 +1510,56 @@ const makeHostService = (binding) => {
1270
1510
  const state = {
1271
1511
  closed: { value: false },
1272
1512
  disconnected: { value: false },
1273
- uncertain: { value: false },
1513
+ uncertain: { value: pendingInput || resume !== void 0 && resume.inputState !== "idle" },
1514
+ pendingInputs: /* @__PURE__ */ new Set(),
1515
+ actions: { value: resume?.consumedActions ?? 0 },
1274
1516
  violation: { value: void 0 },
1275
1517
  pendingRequests: /* @__PURE__ */ new Set()
1276
1518
  };
1277
- const lifecycle = { managedTeardownInstalled: false };
1519
+ const lifecycle = {
1520
+ managedTeardownInstalled: false,
1521
+ retained: resume !== void 0
1522
+ };
1278
1523
  const closers = [];
1279
- const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(entry.warning))))));
1280
- const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1524
+ const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : entry.close.pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(entry.warning))))));
1525
+ const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(resume === void 0 ? Effect.tryPromise({
1281
1526
  try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.acquire(keepAliveMillis(fixedPolicy)), (id) => runCleanup(Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(Effect.flatMap((value) => terminate(Redacted.make(value), [])))))),
1282
1527
  catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
1283
1528
  implementation: browserRunInteractiveImplementation,
1284
1529
  message: "Browser Run has no capacity for a new browser session"
1285
1530
  }) : protocolError("Acquiring the Browser Run session failed", cause)
1286
- }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
1531
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))) : Effect.succeed(Redacted.value(resume.sessionId)), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
1287
1532
  state.closed.value = true;
1288
1533
  state.disconnected.value = true;
1289
- return lifecycle.managedTeardownInstalled ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
1534
+ return lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
1290
1535
  }), { interruptible: true });
1536
+ const owner = Symbol();
1537
+ const releaseOwner = () => {
1538
+ if (owners.get(sessionIdValue) === owner) owners.delete(sessionIdValue);
1539
+ };
1291
1540
  const connect = yield* Effect.cached(Effect.gen(function* () {
1541
+ if (owners.has(sessionIdValue)) return yield* InteractiveBrowserBusyError.make({
1542
+ implementation: browserRunInteractiveImplementation,
1543
+ message: "The previous local browser owner must detach before resuming"
1544
+ });
1545
+ yield* Effect.acquireRelease(Effect.sync(() => {
1546
+ owners.set(sessionIdValue, owner);
1547
+ }), () => Effect.sync(() => {
1548
+ if (!lifecycle.managedTeardownInstalled) releaseOwner();
1549
+ }));
1292
1550
  const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1293
- try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(closeAcquired(acquired)))),
1294
- catch: (cause) => protocolError("Connecting to the Browser Run session failed", cause)
1295
- }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the local browser connection failed")), { interruptible: true });
1551
+ try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(resume === void 0 ? closeAcquired(acquired) : closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close))),
1552
+ catch: (cause) => resume !== void 0 && cause instanceof BrowserRunFailure && (cause.status === 404 || cause.status === 410) ? expiredError() : protocolError("Connecting to the Browser Run session failed", cause)
1553
+ }).pipe(Effect.timeoutOrElse({
1554
+ duration: "10 seconds",
1555
+ orElse: () => Effect.fail(protocolError("Connecting to the Browser Run session timed out"))
1556
+ })), fixedPolicy, startedAt), (acquired) => lifecycle.managedTeardownInstalled ? Effect.void : lifecycle.retained ? closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close.pipe(Effect.tap(() => Effect.sync(releaseOwner)), Effect.catchCause((cause) => reportBrowserCause("interactive.detach", cause))) : releaseBeforeManaged(closeEntry(acquired.close, "Closing the local browser connection failed")), { interruptible: true });
1557
+ if (resume !== void 0) {
1558
+ if ((yield* Effect.try({
1559
+ try: browser.sessionId,
1560
+ catch: (cause) => protocolError("Reading the reattached browser identity failed", cause)
1561
+ })) !== sessionIdValue) return yield* protocolError("The reattached browser session identity did not match");
1562
+ }
1296
1563
  closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
1297
1564
  const disconnected = () => {
1298
1565
  state.disconnected.value = true;
@@ -1309,21 +1576,32 @@ const makeHostService = (binding) => {
1309
1576
  state.disconnected.value = true;
1310
1577
  return yield* expiredError();
1311
1578
  }
1312
- const context = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1579
+ const reattached = resume === void 0 ? void 0 : yield* withinDeadline(Effect.tryPromise({
1580
+ try: async () => {
1581
+ if (browser.reattach === void 0 || browser.detach === void 0) throw new Error("The browser binding does not support exact reattachment");
1582
+ return await browser.reattach(resume.page);
1583
+ },
1584
+ catch: (cause) => protocolError("Reattaching the exact browser page failed", cause)
1585
+ }).pipe(Effect.timeoutOrElse({
1586
+ duration: "10 seconds",
1587
+ orElse: () => Effect.fail(protocolError("Reattaching the exact browser page timed out"))
1588
+ })), fixedPolicy, startedAt);
1589
+ if (resume !== void 0 && reattached === void 0) return yield* expiredError();
1590
+ const context = reattached?.context ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1313
1591
  try: (signal) => observeLate(signal, closeLateAcquisition(signal, browser.createContext, (acquired) => runCleanup(Effect.tryPromise({
1314
1592
  try: () => acquired.close(),
1315
1593
  catch: (cause) => actionError("close", cause)
1316
1594
  })))),
1317
1595
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
1318
- }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true });
1596
+ }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true }));
1319
1597
  closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
1320
- const page = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1598
+ const page = reattached?.page ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1321
1599
  try: (signal) => observeLate(signal, closeLateAcquisition(signal, context.newPage, (acquired) => runCleanup(Effect.tryPromise({
1322
1600
  try: () => acquired.close(),
1323
1601
  catch: (cause) => actionError("close", cause)
1324
1602
  })))),
1325
1603
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
1326
- }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true });
1604
+ }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true }));
1327
1605
  closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
1328
1606
  yield* withinDeadline(Effect.tryPromise({
1329
1607
  try: () => page.setBypassServiceWorker(true),
@@ -1342,27 +1620,66 @@ const makeHostService = (binding) => {
1342
1620
  yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
1343
1621
  const setupFailure = stateFailure(state);
1344
1622
  if (setupFailure !== void 0) return yield* setupFailure;
1623
+ const detach = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
1624
+ if (browser.detach === void 0) return yield* protocolError("The browser binding does not support detachment");
1625
+ lifecycle.retained = true;
1626
+ state.closed.value = true;
1627
+ state.disconnected.value = true;
1628
+ if (state.pendingInputs.size > 0) state.uncertain.value = true;
1629
+ yield* closeEntry(async () => {
1630
+ page.offRequest(requestListener);
1631
+ browser.offDisconnected(disconnected);
1632
+ await browser.detach?.();
1633
+ }, "Detaching the browser transport failed").close;
1634
+ releaseOwner();
1635
+ }).pipe(Effect.catchTag("InteractiveBrowserActionError", (error) => reportBrowserCause("interactive.detach", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError("close")))))), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.detach", error)))));
1345
1636
  const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
1346
1637
  const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)), Effect.mapError(reportedBrowserError)));
1347
1638
  lifecycle.managedTeardownInstalled = true;
1639
+ lifecycle.retained = false;
1348
1640
  yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
1349
1641
  state.closed.value = true;
1350
1642
  state.disconnected.value = true;
1351
- }).pipe(Effect.andThen(cached), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
1643
+ }).pipe(Effect.andThen(Effect.suspend(() => lifecycle.retained ? detach : cached)), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
1352
1644
  return cached;
1353
1645
  }));
1354
- const close = Effect.uninterruptible(Effect.sync(() => {
1646
+ const close = Effect.uninterruptible(Effect.suspend(() => {
1647
+ if (lifecycle.retained || owners.get(sessionIdValue) !== owner) return Effect.void;
1355
1648
  state.closed.value = true;
1356
1649
  state.disconnected.value = true;
1357
- }).pipe(Effect.andThen(teardown)));
1650
+ return teardown;
1651
+ }));
1358
1652
  const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
1359
1653
  const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
1360
1654
  const requestFitsSession = (requestedMillis) => remainingMillis(fixedPolicy, startedAt).pipe(Effect.flatMap((remaining) => remaining > 0 && requestedMillis <= remaining ? Effect.void : Effect.fail(policyError("The host browser request exceeds the remaining session time"))));
1655
+ const inputState = Effect.sync(() => state.uncertain.value ? "unknown" : state.pendingInputs.size > 0 ? "running" : "idle");
1656
+ const pageIdentity = yield* Effect.cached(Effect.tryPromise({
1657
+ try: async () => {
1658
+ if (page.identity === void 0) throw new Error("The browser binding does not expose page identity");
1659
+ return await page.identity();
1660
+ },
1661
+ catch: (cause) => protocolError("Reading the exact browser page identity failed", cause)
1662
+ }).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.checkpoint", error))));
1663
+ const observeResize = yield* makeHostInputObserver(state, "interactive.resize.late");
1361
1664
  return {
1362
1665
  handle: runtime.handle,
1666
+ checkpoint: withinDeadline(Effect.gen(function* () {
1667
+ const identity = resume?.page ?? (yield* pageIdentity);
1668
+ return BrowserRunInteractiveCheckpoint.make({
1669
+ sessionId: Redacted.make(sessionIdValue),
1670
+ page: identity,
1671
+ policy: InteractiveBrowserPolicy.make(fixedPolicy),
1672
+ startedAt,
1673
+ consumedActions: state.actions.value,
1674
+ inputState: yield* inputState
1675
+ });
1676
+ }), fixedPolicy, startedAt),
1677
+ detach,
1678
+ inputState,
1679
+ drainInput: Effect.suspend(() => Effect.promise(() => boundedBestEffort(Promise.allSettled(state.pendingInputs), 500)).pipe(Effect.andThen(inputState))),
1363
1680
  sessionId: Redacted.make(sessionIdValue),
1364
1681
  resizeViewport: (viewport) => decodeViewport(viewport).pipe(Effect.flatMap((decoded) => runtime.run(Effect.tryPromise({
1365
- try: () => page.setViewport(decoded),
1682
+ try: (signal) => observeResize(page.setViewport(decoded), signal),
1366
1683
  catch: (cause) => {
1367
1684
  if (state.disconnected.value || isRemoteClosure(cause)) {
1368
1685
  state.disconnected.value = true;
@@ -1370,11 +1687,11 @@ const makeHostService = (binding) => {
1370
1687
  }
1371
1688
  return protocolError("Resizing the browser viewport failed", cause);
1372
1689
  }
1373
- }), currentPagePreflight, false))),
1690
+ }).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resize", error))), currentPagePreflight, false))),
1374
1691
  getLiveView: (request) => Schema.decodeEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => policyError("The Live View request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.getLiveView", {
1375
1692
  mode: decoded.mode,
1376
1693
  expiresInMs: decoded.expiresInMs
1377
- }, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs)))))),
1694
+ }, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs))), true, true))),
1378
1695
  handoff: (request) => Schema.decodeEffect(BrowserRunHandoffRequest)(request).pipe(Effect.mapError(() => policyError("The browser handoff request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.handoff", {
1379
1696
  instructions: decoded.instructions,
1380
1697
  timeout: decoded.timeout
@@ -1383,7 +1700,7 @@ const makeHostService = (binding) => {
1383
1700
  active: observation.active,
1384
1701
  ...observation.handoffId === void 0 ? {} : { handoffId: Redacted.make(observation.handoffId) },
1385
1702
  ...observation.durationMs === void 0 ? {} : { durationMs: observation.durationMs }
1386
- }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight),
1703
+ }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight, true, false),
1387
1704
  close
1388
1705
  };
1389
1706
  }).pipe(Effect.provideService(Scope.Scope, scope)));
@@ -1395,6 +1712,11 @@ const makeHostService = (binding) => {
1395
1712
  return BrowserRunInteractiveHost.of({
1396
1713
  acquire,
1397
1714
  open: (policy) => acquire(policy).pipe(Effect.flatMap((acquired) => acquired.connect)),
1715
+ resume: (checkpoint, options) => Schema.decodeEffect(Schema.toType(BrowserRunInteractiveCheckpoint))(checkpoint).pipe(Effect.mapError(() => policyError("The browser checkpoint is malformed")), Effect.flatMap((decoded) => acquire(decoded.policy, decoded, options.pendingInput)), Effect.flatMap((acquired) => acquired.connect), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resume", error, {
1716
+ stage: "preparation",
1717
+ dispatch: "not-dispatched",
1718
+ session: "disconnected"
1719
+ }))),
1398
1720
  closeSession,
1399
1721
  cleanupSemantics: "confirmed-terminal"
1400
1722
  });
@@ -1422,6 +1744,6 @@ const CloudflareInteractiveBrowser = {
1422
1744
  hostLayer: (options) => browserRunInteractiveHostLayer().pipe(Layer.provide(interactiveBindingLayer(options)))
1423
1745
  };
1424
1746
  //#endregion
1425
- export { BrowserRunFailure as _, BrowserRunInteractiveHost as a, reportBrowserCause as b, BrowserRunViewport as c, browserRunInteractiveImplementation as d, browserRunInteractiveLayer as f, BrowserRunBinding as g, BrowserRunSessionLifecycle as h, BrowserRunInteractiveBinding as i, CloudflareInteractiveBrowser as l, BrowserRunCleanupError as m, BrowserRunHandoffResult as n, BrowserRunLiveViewRequest as o, isBrowserRunUndispatchedActionError as p, BrowserRunHandoffState as r, BrowserRunLiveViewResult as s, BrowserRunHandoffRequest as t, browserRunInteractiveHostLayer as u, browserFailure as v, reportedBrowserError as x, inheritBrowserReport as y };
1747
+ export { reportBrowserCause as C, inheritBrowserReport as S, BrowserRunCleanupError as _, BrowserRunInteractiveCheckpoint as a, BrowserRunFailure as b, BrowserRunLiveViewResult as c, BrowserRunViewport as d, CloudflareInteractiveBrowser as f, isBrowserRunUndispatchedActionError as g, browserRunInteractiveLayer as h, BrowserRunInteractiveBinding as i, BrowserRunPageIdentity as l, browserRunInteractiveImplementation as m, BrowserRunHandoffResult as n, BrowserRunInteractiveHost as o, browserRunInteractiveHostLayer as p, BrowserRunHandoffState as r, BrowserRunLiveViewRequest as s, BrowserRunHandoffRequest as t, BrowserRunPageObservation as u, BrowserRunSessionLifecycle as v, reportedBrowserError as w, browserFailure as x, BrowserRunBinding as y };
1426
1748
 
1427
- //# sourceMappingURL=InteractiveBrowser-l6J4eP6T.mjs.map
1749
+ //# sourceMappingURL=InteractiveBrowser-CBUFAECo.mjs.map