@effect-agent/platform-cloudflare 0.1.0-beta.106 → 0.1.0-beta.108

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,173 @@
1
- import { Clock, Context, Duration, Effect, Layer, Option, Redacted, Ref, Schema, Scope, Semaphore, Stream } from "effect";
1
+ import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted, Ref, 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, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
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";
7
+ //#region src/internal/browser-failure.ts
8
+ /** Source-authored stages and classifications only; never retain SDK text or session capabilities. */
9
+ var BrowserRunFailure = class extends Data.TaggedError("BrowserRunFailure") {
10
+ [ErrorReporter.severity] = "Error";
11
+ };
12
+ /** A public projection of an already reported private failure must not produce another issue. */
13
+ const reportedBrowserError = (error) => Object.assign(error, { [ErrorReporter.ignore]: true });
14
+ const inheritBrowserReport = (original, error) => ErrorReporter.isIgnored(original) ? reportedBrowserError(error) : error;
15
+ const reasonSchema = Schema.Literals([
16
+ "provider",
17
+ "timeout",
18
+ "malformed",
19
+ "configuration",
20
+ "authorization",
21
+ "rate-limited",
22
+ "pending"
23
+ ]);
24
+ const statusSchema = Schema.Int.check(Schema.isBetween({
25
+ minimum: 100,
26
+ maximum: 599
27
+ }));
28
+ const failureNames = /* @__PURE__ */ new Set([
29
+ "TypeError",
30
+ "RangeError",
31
+ "SyntaxError",
32
+ "TimeoutError",
33
+ "SchemaError"
34
+ ]);
35
+ const browserFailure = (operation, cause) => cause instanceof BrowserRunFailure ? cause : new BrowserRunFailure({
36
+ operation,
37
+ reason: Predicate.isObject(cause) && "reason" in cause && Schema.is(reasonSchema)(cause.reason) ? cause.reason : "provider",
38
+ ...Predicate.isObject(cause) && "status" in cause && Schema.is(statusSchema)(cause.status) ? { status: cause.status } : {},
39
+ ...Predicate.isObject(cause) && "cause" in cause && cause.cause instanceof BrowserRunFailure ? { cause: cause.cause } : Predicate.isObject(cause) && "name" in cause && typeof cause.name === "string" && failureNames.has(cause.name) ? { cause: { name: cause.name } } : {}
40
+ });
41
+ /** Keep mixed defects and interruptions distinct while discarding all foreign diagnostic content. */
42
+ const reportBrowserCause = (operation, cause) => ErrorReporter.report(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isFailReason(reason) || !ErrorReporter.isIgnored(reason.error)).map((reason) => {
43
+ switch (reason._tag) {
44
+ case "Fail": return Cause.makeFailReason(browserFailure(operation, reason.error));
45
+ case "Die": return Cause.makeDieReason(browserFailure(operation, reason.defect));
46
+ case "Interrupt": return reason;
47
+ }
48
+ })));
49
+ //#endregion
50
+ //#region src/internal/browser-binding.ts
51
+ const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
52
+ /** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
53
+ var BrowserRunBinding = class extends Context.Service()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
54
+ static layer(browser) {
55
+ return Layer.succeed(this)({
56
+ acquire: async (keepAliveMillis, operation) => {
57
+ const response = await browser.fetch(`https://browser-rendering.cloudflare.com/v1/devtools/browser?keep_alive=${keepAliveMillis}&recording=false`, { method: "POST" });
58
+ if (response.status !== 200) {
59
+ const failure = new BrowserRunFailure({
60
+ operation,
61
+ reason: "provider",
62
+ status: response.status
63
+ });
64
+ try {
65
+ await response.body?.cancel();
66
+ } catch {}
67
+ throw failure;
68
+ }
69
+ return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
70
+ },
71
+ connect: async (sessionId, operation) => {
72
+ let failure;
73
+ try {
74
+ return await puppeteer.connect({ fetch: async (input, init) => {
75
+ try {
76
+ const response = await browser.fetch(input, init);
77
+ if (response.status !== 101) {
78
+ failure = new BrowserRunFailure({
79
+ operation,
80
+ reason: "provider",
81
+ status: response.status
82
+ });
83
+ await response.body?.cancel();
84
+ throw failure;
85
+ }
86
+ return response;
87
+ } catch (cause) {
88
+ failure ??= browserFailure(operation, cause);
89
+ throw failure;
90
+ }
91
+ } }, sessionId);
92
+ } catch (cause) {
93
+ throw failure ?? browserFailure(operation, cause);
94
+ }
95
+ }
96
+ });
97
+ }
98
+ };
99
+ //#endregion
100
+ //#region src/internal/browser-file-selection.ts
101
+ function setFileSelection(element, encoded, name, type) {
102
+ if (Reflect.get(element, "type") !== "file" || Reflect.get(element, "disabled") || Reflect.get(element, "webkitdirectory")) throw new Error("The target is not an enabled file input");
103
+ const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
104
+ const transfer = new (Reflect.get(globalThis, "DataTransfer"))();
105
+ transfer.items.add(new File([bytes], name, { type }));
106
+ Reflect.set(element, "files", transfer.files);
107
+ const selected = Reflect.get(element, "files");
108
+ if (selected?.length !== 1 || selected[0]?.size !== bytes.length || selected[0]?.name !== name || selected[0]?.type !== type) throw new Error("The input did not retain the selected file");
109
+ const dispatch = Reflect.get(element, "dispatchEvent");
110
+ Reflect.apply(dispatch, element, [new Event("input", { bubbles: true })]);
111
+ Reflect.apply(dispatch, element, [new Event("change", { bubbles: true })]);
112
+ }
113
+ const makeFileSelection = (page) => async (request, signal) => {
114
+ const encoded = Encoding.encodeBase64(request.bytes);
115
+ const session = request.target === "chooser" ? await page.createCDPSession() : void 0;
116
+ if (session !== void 0) try {
117
+ await session.send("Page.enable");
118
+ await session.send("Page.setInterceptFileChooserDialog", { enabled: true });
119
+ } catch (cause) {
120
+ await session.detach();
121
+ throw cause;
122
+ }
123
+ const select = async (element) => {
124
+ if (session === void 0) return element.evaluate(setFileSelection, encoded, request.fileName, request.mediaType);
125
+ let onOpened = () => {};
126
+ let onAbort = () => {};
127
+ let timer;
128
+ const opened = new Promise((resolve, reject) => {
129
+ onOpened = (event) => event.backendNodeId === void 0 ? reject(/* @__PURE__ */ new Error("The chooser has no file input")) : resolve(event.backendNodeId);
130
+ onAbort = () => reject(/* @__PURE__ */ new Error("File selection interrupted"));
131
+ session.on("Page.fileChooserOpened", onOpened);
132
+ signal.addEventListener("abort", onAbort, { once: true });
133
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("No file chooser opened")), 5e3);
134
+ if (signal.aborted) onAbort();
135
+ });
136
+ try {
137
+ const [backendNodeId] = await Promise.all([opened, element.click()]);
138
+ if (signal.aborted) throw new Error("File selection interrupted");
139
+ const objectId = (await session.send("DOM.resolveNode", { backendNodeId })).object.objectId;
140
+ if (objectId === void 0) throw new Error("The chooser has no file input");
141
+ if (signal.aborted) throw new Error("File selection interrupted");
142
+ if ((await session.send("Runtime.callFunctionOn", {
143
+ objectId,
144
+ functionDeclaration: `function(encoded, name, type) { (${setFileSelection.toString()})(this, encoded, name, type); }`,
145
+ arguments: [
146
+ { value: encoded },
147
+ { value: request.fileName },
148
+ { value: request.mediaType }
149
+ ],
150
+ returnByValue: true
151
+ })).exceptionDetails !== void 0) throw new Error("The chooser did not select the file");
152
+ } finally {
153
+ clearTimeout(timer);
154
+ session.off("Page.fileChooserOpened", onOpened);
155
+ signal.removeEventListener("abort", onAbort);
156
+ }
157
+ };
158
+ return {
159
+ validate: async (element) => request.target === "chooser" || await element.evaluate((input) => Reflect.get(input, "type") === "file" && !Reflect.get(input, "disabled") && !Reflect.get(input, "webkitdirectory")),
160
+ select,
161
+ close: async () => {
162
+ if (session !== void 0) try {
163
+ await session.send("Page.setInterceptFileChooserDialog", { enabled: false });
164
+ } finally {
165
+ await session.detach();
166
+ }
167
+ }
168
+ };
169
+ };
170
+ //#endregion
7
171
  //#region src/internal/browser-session-lifecycle.ts
8
172
  var BrowserRunCleanupError = class extends Schema.TaggedError()("BrowserRunCleanupError", {
9
173
  reason: Schema.Literals([
@@ -15,7 +179,8 @@ var BrowserRunCleanupError = class extends Schema.TaggedError()("BrowserRunClean
15
179
  "timeout",
16
180
  "pending"
17
181
  ]),
18
- status: Schema.optionalKey(Schema.Int)
182
+ status: Schema.optionalKey(Schema.Int),
183
+ cause: Schema.optionalKey(Schema.Defect())
19
184
  }) {};
20
185
  const Identity = Schema.String.check(Schema.isUUID());
21
186
  const Account = Schema.String.check(Schema.isPattern(/^[a-f0-9]{32}$/));
@@ -32,7 +197,10 @@ var BrowserRunSessionLifecycle = class extends Context.Service()("@effect-agent/
32
197
  const client = yield* HttpClient.HttpClient;
33
198
  const request = Effect.fn("BrowserRunSessionLifecycle.request")(function* (method, sessionId) {
34
199
  const path = method === "DELETE" ? "browser" : "session";
35
- const response = yield* client.execute(HttpClientRequest.make(method)(`https://api.cloudflare.com/client/v4/accounts/${options.accountId}/browser-rendering/devtools/${path}/${sessionId}`).pipe(HttpClientRequest.bearerToken(options.apiToken))).pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), Effect.mapError(() => new BrowserRunCleanupError({ reason: "provider" })));
200
+ const response = yield* client.execute(HttpClientRequest.make(method)(`https://api.cloudflare.com/client/v4/accounts/${options.accountId}/browser-rendering/devtools/${path}/${sessionId}`).pipe(HttpClientRequest.bearerToken(options.apiToken))).pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), Effect.mapError((cause) => new BrowserRunCleanupError({
201
+ reason: "provider",
202
+ cause: browserFailure("browser.close.request", cause)
203
+ })));
36
204
  if (response.status === 401 || response.status === 403) return yield* new BrowserRunCleanupError({
37
205
  reason: "authorization",
38
206
  status: response.status
@@ -52,21 +220,33 @@ var BrowserRunSessionLifecycle = class extends Context.Service()("@effect-agent/
52
220
  combined.set(body);
53
221
  combined.set(chunk, body.byteLength);
54
222
  return Effect.succeed(combined);
55
- }).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
223
+ }).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
224
+ reason: "malformed",
225
+ cause: browserFailure("browser.close.response", cause)
226
+ })));
56
227
  const body = yield* Effect.try({
57
228
  try: () => new TextDecoder("utf-8", {
58
229
  fatal: true,
59
230
  ignoreBOM: false
60
231
  }).decode(bytes),
61
- catch: () => new BrowserRunCleanupError({ reason: "malformed" })
232
+ catch: (cause) => new BrowserRunCleanupError({
233
+ reason: "malformed",
234
+ cause: browserFailure("browser.close.decode", cause)
235
+ })
62
236
  });
63
237
  if (response.status === 404) {
64
238
  const absent = Schema.decodeOption(Schema.fromJsonString(Absent))(body, { onExcessProperty: "error" });
65
239
  if (Option.isSome(absent)) return true;
66
240
  return yield* new BrowserRunCleanupError({ reason: "malformed" });
67
241
  }
68
- if (method === "DELETE") return (yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })))).status === "closed";
69
- const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "malformed" })));
242
+ if (method === "DELETE") return (yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
243
+ reason: "malformed",
244
+ cause: browserFailure("browser.close.response", cause)
245
+ })))).status === "closed";
246
+ const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
247
+ reason: "malformed",
248
+ cause: browserFailure("browser.close.response", cause)
249
+ })));
70
250
  if (result.sessionId !== sessionId) return yield* new BrowserRunCleanupError({ reason: "malformed" });
71
251
  return result.endTime !== void 0 && result.endTime > 0;
72
252
  });
@@ -231,13 +411,14 @@ var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Co
231
411
  static layer(options) {
232
412
  return Layer.effect(BrowserRunInteractiveBinding)(Effect.gen(function* () {
233
413
  const lifecycle = yield* BrowserRunSessionLifecycle;
414
+ const binding = yield* BrowserRunBinding;
234
415
  const viewport = options.viewport === void 0 ? void 0 : yield* decodeViewport(options.viewport);
235
416
  return {
236
- acquire: async (keepAliveMillis) => (await puppeteer.acquire(options.browser, { keep_alive: keepAliveMillis })).sessionId,
237
- connect: async (sessionId) => makeProductionBrowser(await puppeteer.connect(options.browser, sessionId), viewport),
417
+ acquire: async (keepAliveMillis) => binding.acquire(keepAliveMillis, "interactive.acquire"),
418
+ connect: async (sessionId) => makeProductionBrowser(await binding.connect(sessionId, "interactive.connect"), viewport),
238
419
  closeSession: (sessionId) => lifecycle.close(sessionId).pipe(Effect.mapError((cause) => actionError("close", cause)))
239
420
  };
240
- }));
421
+ })).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
241
422
  }
242
423
  };
243
424
  /** Cloudflare host authority kept separate from the provider-neutral browser handle. */
@@ -413,7 +594,7 @@ const makeActionRequestTracker = (page, signal) => {
413
594
  const disposeActionHandles = async (handles) => {
414
595
  await boundedBestEffort(Promise.allSettled(handles.map(async (handle) => handle.dispose())), ACTION_POST_STATE_MILLIS);
415
596
  };
416
- const runObservedPageAction = async (page, selector, signal, onDispatch, action) => {
597
+ const runObservedPageAction = async (page, selector, signal, onDispatch, action, validate) => {
417
598
  if (signal.aborted) throw new BrowserRunActionUndispatched(0);
418
599
  const before = Schema.decodeUnknownSync(ActionTargetState)(await readActionTarget(page, selector));
419
600
  if (before.matchCount !== 1 || before.invalidSelector === true) throw new BrowserRunActionUndispatched(before.matchCount);
@@ -434,6 +615,7 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action)
434
615
  };
435
616
  signal.addEventListener("abort", onAbort, { once: true });
436
617
  try {
618
+ if (validate !== void 0 && !await validate(matches[0])) throw new BrowserRunActionUndispatched(1);
437
619
  tracker = makeActionRequestTracker(page, signal);
438
620
  if (signal.aborted) throw new BrowserRunActionUndispatched(1);
439
621
  onDispatch();
@@ -454,9 +636,17 @@ const runObservedPageAction = async (page, selector, signal, onDispatch, action)
454
636
  }
455
637
  };
456
638
  const makeProductionPage = (page) => {
639
+ const prepareFileSelection = makeFileSelection(page);
457
640
  const listeners = /* @__PURE__ */ new Map();
458
641
  return {
459
- close: () => page.close(),
642
+ close: async () => {
643
+ if (!page.browser().isConnected()) return;
644
+ try {
645
+ await page.close();
646
+ } catch (cause) {
647
+ if (!isRemoteClosure(cause)) throw cause;
648
+ }
649
+ },
460
650
  setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
461
651
  setRequestInterception: (enabled) => page.setRequestInterception(enabled),
462
652
  onRequest: (listener) => {
@@ -613,6 +803,14 @@ const makeProductionPage = (page) => {
613
803
  }
614
804
  }, value)),
615
805
  click: (selector, signal, onDispatch) => runObservedPageAction(page, selector, signal, onDispatch, (element) => element.click()),
806
+ selectFile: async (request, signal, onDispatch) => {
807
+ const selection = await prepareFileSelection(request, signal);
808
+ try {
809
+ return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate);
810
+ } finally {
811
+ await selection.close();
812
+ }
813
+ },
616
814
  screenshot: (fullPage) => page.screenshot({
617
815
  type: "png",
618
816
  fullPage
@@ -635,11 +833,25 @@ const makeProductionContext = (context, viewport) => ({
635
833
  if (viewport !== void 0) await page.setViewport(viewport);
636
834
  return makeProductionPage(page);
637
835
  },
638
- close: () => context.close()
836
+ close: async () => {
837
+ if (!context.browser().isConnected()) return;
838
+ try {
839
+ await context.close();
840
+ } catch (cause) {
841
+ if (!isRemoteClosure(cause)) throw cause;
842
+ }
843
+ }
639
844
  });
640
845
  const makeProductionBrowser = (browser, viewport) => ({
641
846
  createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
642
- close: () => browser.close(),
847
+ close: async () => {
848
+ if (!browser.isConnected()) return;
849
+ try {
850
+ await browser.close();
851
+ } catch (cause) {
852
+ if (!isRemoteClosure(cause)) throw cause;
853
+ }
854
+ },
643
855
  sessionId: () => browser.sessionId(),
644
856
  isConnected: () => browser.isConnected(),
645
857
  onDisconnected: (listener) => {
@@ -679,7 +891,7 @@ const causeText = (cause) => {
679
891
  if (cause instanceof Error) return cause.message.slice(0, 8e3);
680
892
  return String(cause).slice(0, 8e3);
681
893
  };
682
- const isCapacityRefusal = (cause) => /(^|\D)429(\D|$)|browser time limit|capacity|too many concurrent/i.test(causeText(cause));
894
+ const isCapacityRefusal = (cause) => cause instanceof BrowserRunFailure && cause.status === 429;
683
895
  const isRemoteClosure = (cause) => /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(causeText(cause));
684
896
  const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (input) {
685
897
  const decoded = yield* Schema.decodeEffect(InteractiveBrowserPolicy, { onExcessProperty: "error" })(input).pipe(Effect.mapError(() => policyError("The interactive browser policy is malformed")));
@@ -708,21 +920,22 @@ const hostAllowed = (policy, value) => {
708
920
  }
709
921
  };
710
922
  const keepAliveMillis = (policy) => Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
923
+ var LateBrowserAcquisition = class extends Error {};
711
924
  const closeLateAcquisition = async (signal, acquire, close) => {
712
925
  const acquired = await acquire();
713
926
  if (!signal.aborted) return acquired;
714
927
  try {
715
928
  await close(acquired);
716
929
  } catch {}
717
- throw new Error("The interrupted browser acquisition completed late");
930
+ throw new LateBrowserAcquisition("The interrupted browser acquisition completed late");
718
931
  };
719
932
  const closeWithWarning = (close, warning) => Effect.tryPromise({
720
933
  try: close,
721
- catch: () => protocolError(warning)
934
+ catch: (cause) => protocolError(warning, cause)
722
935
  }).pipe(Effect.timeoutOrElse({
723
936
  duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
724
937
  orElse: () => Effect.fail(protocolError(warning))
725
- }), Effect.catchCause(() => Effect.logWarning(warning)));
938
+ }), Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(warning)))));
726
939
  const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (policy, startedAt) {
727
940
  const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
728
941
  return yield* InteractiveBrowserLimitError.make({
@@ -786,12 +999,12 @@ const makeRequestListener = (policy, state) => (request) => {
786
999
  let settlement;
787
1000
  try {
788
1001
  settlement = allowed ? request.continue() : request.abort();
789
- } catch {
790
- state.violation.value = protocolError("Resolving an intercepted browser request failed");
1002
+ } catch (cause) {
1003
+ state.violation.value = protocolError("Resolving an intercepted browser request failed", cause);
791
1004
  return;
792
1005
  }
793
- const observed = settlement.catch(() => {
794
- state.violation.value = protocolError("Resolving an intercepted browser request failed");
1006
+ const observed = settlement.catch((cause) => {
1007
+ state.violation.value = protocolError("Resolving an intercepted browser request failed", cause);
795
1008
  }).finally(() => {
796
1009
  state.pendingRequests.delete(observed);
797
1010
  });
@@ -941,6 +1154,17 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page
941
1154
  yield* logActionObservation("click", observation);
942
1155
  return yield* decodeActionResult(page, policy);
943
1156
  })),
1157
+ 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));
1159
+ yield* logActionObservation("select-file", observation);
1160
+ const result = yield* decodeActionResult(page, policy);
1161
+ return BrowserFileSelectionResult.make({
1162
+ url: result.url,
1163
+ fileName: decoded.fileName,
1164
+ mediaType: decoded.mediaType,
1165
+ size: decoded.bytes.length
1166
+ });
1167
+ })))),
944
1168
  screenshot: (request) => Schema.decodeEffect(BrowserScreenshotRequest)(request).pipe(Effect.mapError(() => policyError("The browser screenshot request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
945
1169
  const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
946
1170
  const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
@@ -992,15 +1216,25 @@ const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry)
992
1216
  onSuccess: () => void 0
993
1217
  }))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
994
1218
  const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
1219
+ const cleanupContext = yield* Effect.context();
995
1220
  const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
996
- try: (signal) => closeLateAcquisition(signal, page.createCdpSession, (acquired) => acquired.detach()),
1221
+ try: (signal) => {
1222
+ const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) => Effect.runPromiseWith(cleanupContext)(Effect.tryPromise({
1223
+ try: () => acquired.detach(),
1224
+ catch: (cause) => actionError("close", cause)
1225
+ }).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateDetach", cause)))));
1226
+ pending.then(void 0, (cause) => {
1227
+ if (signal.aborted && !(cause instanceof LateBrowserAcquisition)) return Effect.runPromiseWith(cleanupContext)(reportBrowserCause("interactive.connect", Cause.fail(cause)));
1228
+ });
1229
+ return pending;
1230
+ },
997
1231
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
998
1232
  }), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
999
1233
  const raw = yield* Effect.tryPromise({
1000
1234
  try: () => cdp.send(command, parameters),
1001
1235
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
1002
1236
  });
1003
- return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.mapError(() => protocolError(malformedMessage)));
1237
+ 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)))))));
1004
1238
  }));
1005
1239
  const makeHostService = (binding) => {
1006
1240
  const closeSession = binding.closeSession;
@@ -1012,20 +1246,27 @@ const makeHostService = (binding) => {
1012
1246
  }));
1013
1247
  const remaining = deadline - (yield* Clock.currentTimeMillis);
1014
1248
  if (remaining <= 0) return yield* Effect.logWarning("Local browser teardown skipped after confirmed termination deadline");
1015
- yield* runTeardown(entries).pipe(Effect.flatMap((failures) => Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning), { discard: true })), Effect.interruptible, Effect.timeout(`${remaining} millis`), Effect.catchCause(() => Effect.logWarning("Local browser teardown incomplete after confirmed termination")));
1249
+ yield* runTeardown(entries).pipe(Effect.flatMap((failures) => Effect.forEach(failures, (failure) => reportBrowserCause("interactive.cleanup", Cause.fail(failure.error)).pipe(Effect.andThen(Effect.logWarning(failure.warning))), { discard: true })), Effect.interruptible, Effect.timeout(`${remaining} millis`), Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning("Local browser teardown incomplete after confirmed termination")))));
1016
1250
  });
1017
1251
  const closeAcquired = Effect.fn("BrowserRunInteractiveHost.closeAcquired")(function* (browser) {
1018
1252
  const sessionId = yield* Effect.try({
1019
1253
  try: browser.sessionId,
1020
- catch: () => actionError("close")
1021
- }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError(() => actionError("close")), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
1254
+ catch: (cause) => actionError("close", cause)
1255
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError((cause) => actionError("close", cause)), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
1022
1256
  yield* terminate(Redacted.make(sessionId), [closeEntry(browser.close, "Closing the local browser connection failed")]);
1023
1257
  });
1024
1258
  const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy) {
1025
1259
  const scope = yield* Scope.Scope;
1026
1260
  const fixedPolicy = yield* snapshotPolicy(policy);
1027
1261
  const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
1028
- const runCleanup = Effect.runPromiseWith(Context.make(Clock.Clock, yield* Clock.Clock));
1262
+ const runCleanup = (effect) => Effect.runPromiseWith(cleanupContext)(effect.pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause))));
1263
+ const cleanupContext = yield* Effect.context();
1264
+ const observeLate = (signal, pending) => {
1265
+ pending.then(void 0, (cause) => {
1266
+ if (signal.aborted && !(cause instanceof LateBrowserAcquisition)) return runCleanup(reportBrowserCause("interactive.acquire", Cause.fail(cause)));
1267
+ });
1268
+ return pending;
1269
+ };
1029
1270
  const state = {
1030
1271
  closed: { value: false },
1031
1272
  disconnected: { value: false },
@@ -1035,9 +1276,9 @@ const makeHostService = (binding) => {
1035
1276
  };
1036
1277
  const lifecycle = { managedTeardownInstalled: false };
1037
1278
  const closers = [];
1038
- const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled ? Effect.void : entry.close.pipe(Effect.catchCause(() => Effect.logWarning(entry.warning))));
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))))));
1039
1280
  const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1040
- try: (signal) => closeLateAcquisition(signal, () => binding.acquire(keepAliveMillis(fixedPolicy)), (id) => runCleanup(Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(Effect.flatMap((value) => terminate(Redacted.make(value), []))))),
1281
+ try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.acquire(keepAliveMillis(fixedPolicy)), (id) => runCleanup(Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(Effect.flatMap((value) => terminate(Redacted.make(value), [])))))),
1041
1282
  catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
1042
1283
  implementation: browserRunInteractiveImplementation,
1043
1284
  message: "Browser Run has no capacity for a new browser session"
@@ -1045,11 +1286,11 @@ const makeHostService = (binding) => {
1045
1286
  }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
1046
1287
  state.closed.value = true;
1047
1288
  state.disconnected.value = true;
1048
- return lifecycle.managedTeardownInstalled ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catch(() => Effect.logWarning("Whole-browser cleanup remains unconfirmed")));
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")))));
1049
1290
  }), { interruptible: true });
1050
1291
  const connect = yield* Effect.cached(Effect.gen(function* () {
1051
1292
  const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1052
- try: (signal) => closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(closeAcquired(acquired))),
1293
+ try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue), (acquired) => runCleanup(closeAcquired(acquired)))),
1053
1294
  catch: (cause) => protocolError("Connecting to the Browser Run session failed", cause)
1054
1295
  }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the local browser connection failed")), { interruptible: true });
1055
1296
  closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
@@ -1069,12 +1310,18 @@ const makeHostService = (binding) => {
1069
1310
  return yield* expiredError();
1070
1311
  }
1071
1312
  const context = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1072
- try: (signal) => closeLateAcquisition(signal, browser.createContext, (acquired) => acquired.close()),
1313
+ try: (signal) => observeLate(signal, closeLateAcquisition(signal, browser.createContext, (acquired) => runCleanup(Effect.tryPromise({
1314
+ try: () => acquired.close(),
1315
+ catch: (cause) => actionError("close", cause)
1316
+ })))),
1073
1317
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
1074
1318
  }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true });
1075
1319
  closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
1076
1320
  const page = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
1077
- try: (signal) => closeLateAcquisition(signal, context.newPage, (acquired) => acquired.close()),
1321
+ try: (signal) => observeLate(signal, closeLateAcquisition(signal, context.newPage, (acquired) => runCleanup(Effect.tryPromise({
1322
+ try: () => acquired.close(),
1323
+ catch: (cause) => actionError("close", cause)
1324
+ })))),
1078
1325
  catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
1079
1326
  }), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true });
1080
1327
  closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
@@ -1096,12 +1343,12 @@ const makeHostService = (binding) => {
1096
1343
  const setupFailure = stateFailure(state);
1097
1344
  if (setupFailure !== void 0) return yield* setupFailure;
1098
1345
  const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
1099
- const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers));
1346
+ const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)), Effect.mapError(reportedBrowserError)));
1100
1347
  lifecycle.managedTeardownInstalled = true;
1101
1348
  yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
1102
1349
  state.closed.value = true;
1103
1350
  state.disconnected.value = true;
1104
- }).pipe(Effect.andThen(cached), Effect.catch(() => Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
1351
+ }).pipe(Effect.andThen(cached), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
1105
1352
  return cached;
1106
1353
  }));
1107
1354
  const close = Effect.uninterruptible(Effect.sync(() => {
@@ -1175,6 +1422,6 @@ const CloudflareInteractiveBrowser = {
1175
1422
  hostLayer: (options) => browserRunInteractiveHostLayer().pipe(Layer.provide(interactiveBindingLayer(options)))
1176
1423
  };
1177
1424
  //#endregion
1178
- export { BrowserRunInteractiveHost as a, BrowserRunViewport as c, browserRunInteractiveImplementation as d, browserRunInteractiveLayer as f, 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 };
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 };
1179
1426
 
1180
- //# sourceMappingURL=InteractiveBrowser-BCS9g0tn.mjs.map
1427
+ //# sourceMappingURL=InteractiveBrowser-l6J4eP6T.mjs.map