@effect-agent/platform-cloudflare 0.1.0-beta.107 → 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.
@@ -705,18 +705,24 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
705
705
  } | {
706
706
  readonly _tag: "AdminFailed";
707
707
  readonly failure: {
708
- readonly _tag: "ThreadStoreError";
708
+ readonly _tag: "DurableAlarmError";
709
709
  readonly operation: string;
710
710
  readonly message: string;
711
711
  readonly cause?: Schema.Json | undefined;
712
712
  } | {
713
- readonly _tag: "ThreadNotMaterialized";
714
- readonly threadId: string;
713
+ readonly _tag: "HostProtocolError";
714
+ readonly message: string;
715
715
  } | {
716
716
  readonly _tag: "LedgerError";
717
717
  readonly operation: string;
718
718
  readonly message: string;
719
719
  readonly cause?: import("effect-agent/failure-diagnostic").Diagnostic | undefined;
720
+ } | {
721
+ readonly _tag: "AdmissionPolicyError";
722
+ readonly reason: "occupied" | "refused" | "unavailable";
723
+ readonly code: string;
724
+ readonly cause?: import("effect-agent/failure-diagnostic").Diagnostic | undefined;
725
+ readonly stack?: string | undefined;
720
726
  } | {
721
727
  readonly _tag: "AppendConflict";
722
728
  readonly threadId: string;
@@ -724,23 +730,6 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
724
730
  readonly reason: "batch-digest" | "record-identity" | "tail";
725
731
  readonly actualTailSequence?: number | undefined;
726
732
  readonly actualTailDigest?: string | undefined;
727
- } | {
728
- readonly _tag: "FenceRejected";
729
- readonly threadId: string;
730
- readonly actualEpoch: number;
731
- readonly attemptedEpoch: number;
732
- } | {
733
- readonly _tag: "OperationDenied";
734
- readonly operation: "abort" | "awaitSettlement" | "explain" | "observe" | "resolveApproval" | "resolveUnknown" | "retry" | "scanObligations" | "verify" | "wake";
735
- readonly reason: string;
736
- readonly threadId?: string | undefined;
737
- readonly submissionId?: string | undefined;
738
- } | {
739
- readonly _tag: "AdmissionPolicyError";
740
- readonly reason: "occupied" | "refused" | "unavailable";
741
- readonly code: string;
742
- readonly cause?: import("effect-agent/failure-diagnostic").Diagnostic | undefined;
743
- readonly stack?: string | undefined;
744
733
  } | {
745
734
  readonly _tag: "DigestError";
746
735
  readonly message: string;
@@ -749,31 +738,42 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
749
738
  readonly _tag: "DurableRuntimeFailpointError";
750
739
  readonly location: "abort:after-intent" | "approval:after-request-append" | "approval:after-suspend" | "checkpoint:after-save" | "checkpoint:before-save" | "claim:after-claim" | "compaction:after-canonical-append" | "compaction:before-canonical-append" | "input:after-canonical-append" | "join:after-canonical-append" | "join:after-claim" | "policy:after-reservation-append" | "policy:before-reservation-append" | "resolve:after-intent" | "run:after-duration-append" | "run:after-start-append" | "run:before-duration-append" | "run:before-start-append" | "step:after-step-append" | "subagent:after-admit" | "subagent:after-child-abort-intent" | "subagent:after-child-ready" | "subagent:after-join-append" | "subagent:after-release" | "subagent:after-release-pending" | "subagent:after-request-append" | "subagent:after-reserve" | "subagent:after-sibling-settle" | "subagent:after-start-append" | "subagent:after-suspend" | "subagent:before-join-append" | "submit:after-admit" | "submit:after-materialize" | "terminalize:after-canonical-append" | "terminalize:after-reserve" | "tools:after-prepared-append" | "tools:after-unavailable-append" | "tools:before-prepared-append" | "tools:before-unavailable-append" | "turn:after-canonical-append" | "turn:after-response-append" | "turn:after-results-append" | "update:after-canonical-append" | "update:after-delivery-insert" | "update:before-canonical-append" | "update:before-delivery-insert" | "worker:after-completion-append" | "worker:after-origin-append" | "worker:after-report-append" | "worker:after-report-delivery" | "worker:after-source-append" | "worker:after-subtree-append" | "worker:before-completion-append" | "worker:before-origin-append" | "worker:before-report-append" | "worker:before-report-delivery" | "worker:before-source-append" | "worker:before-subtree-append";
751
740
  } | {
752
- readonly _tag: "SettlementConflict";
753
- readonly submissionId: string;
754
- readonly existingOutcome: "aborted" | "completed" | "failed";
755
- } | {
756
- readonly _tag: "OwnershipLost";
757
- readonly submissionId: string;
741
+ readonly _tag: "FenceRejected";
742
+ readonly threadId: string;
758
743
  readonly actualEpoch: number;
744
+ readonly attemptedEpoch: number;
759
745
  } | {
760
- readonly _tag: "RunJournalError";
761
- readonly message: string;
762
- readonly cause?: Schema.Json | undefined;
746
+ readonly _tag: "ThreadNotMaterialized";
747
+ readonly threadId: string;
763
748
  } | {
764
- readonly _tag: "DurableAlarmError";
749
+ readonly _tag: "ThreadStoreError";
765
750
  readonly operation: string;
766
751
  readonly message: string;
767
752
  readonly cause?: Schema.Json | undefined;
768
753
  } | {
769
- readonly _tag: "HostProtocolError";
770
- readonly message: string;
754
+ readonly _tag: "OperationDenied";
755
+ readonly operation: "abort" | "awaitSettlement" | "explain" | "observe" | "resolveApproval" | "resolveUnknown" | "retry" | "scanObligations" | "verify" | "wake";
756
+ readonly reason: string;
757
+ readonly threadId?: string | undefined;
758
+ readonly submissionId?: string | undefined;
771
759
  } | {
772
760
  readonly _tag: "RetryRefused";
773
761
  readonly submissionId: string;
774
762
  readonly refusal: "await-approval-decision" | "await-unknown-resolution" | "settled";
775
763
  readonly decisionTag: string;
776
764
  readonly message: string;
765
+ } | {
766
+ readonly _tag: "RunJournalError";
767
+ readonly message: string;
768
+ readonly cause?: Schema.Json | undefined;
769
+ } | {
770
+ readonly _tag: "OwnershipLost";
771
+ readonly submissionId: string;
772
+ readonly actualEpoch: number;
773
+ } | {
774
+ readonly _tag: "SettlementConflict";
775
+ readonly submissionId: string;
776
+ readonly existingOutcome: "aborted" | "completed" | "failed";
777
777
  };
778
778
  }, Schema.SchemaError, never>;
779
779
  declare const decodeAdminResponse: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => Effect.Effect<AdminFailed | ExplainedRecovery | ObligationsScanned | RetryExecuted | VerifiedIntegrity, Schema.SchemaError, never>;
@@ -826,4 +826,4 @@ interface Class<EventServices = never> {
826
826
  declare const make: <ApplicationServices, ApplicationError, EventServices = never, EventLayerError = never>(applicationLayer: Layer.Layer<CloudflareDurableRuntimeServices | ApplicationServices, ApplicationError, CloudflareBootstrapServices | DurableObjectState$1.DurableObjectState | WorkerEnvironment | DurableObjectContext | ThreadObjectNamespace>, options: Options<ApplicationServices, EventServices, EventLayerError>) => Class<ApplicationServices | EventServices>;
827
827
  //#endregion
828
828
  export { ThreadPublicationOptions as A, portCall as C, CloudflareDurableRuntimeOptions as D, CloudflareDurableRuntimeInitializationError as E, layerConfig as M, layerHostConfig as N, CloudflareDurableRuntimeServices as O, layerInHost as P, make as S, CloudflareBootstrapServices as T, decodeAdminVerifyRequest as _, AdminVerifyRequest as a, encodeAdminResponse as b, Instance as c, RetryExecuted as d, ThreadObject_d_exports as f, decodeAdminResponse as g, decodeAdminExplainRequest as h, AdminResponse as i, layer as j, ThreadObjectPorts as k, ObligationsScanned as l, VerifiedIntegrity as m, AdminFailed as n, Class as o, ThreadRpcOperation as p, AdminFailure as r, ExplainedRecovery as s, AdminExplainRequest as t, Options as u, decodeObligationThresholds as v, submit as w, handleRpc as x, decodeRetryCommand as y };
829
- //# sourceMappingURL=ThreadObject-CnqCuWlA.d.mts.map
829
+ //# sourceMappingURL=ThreadObject-D_vvj7Qx.d.mts.map
@@ -1,2 +1,2 @@
1
- import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-CnqCuWlA.mjs";
1
+ import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-D_vvj7Qx.mjs";
2
2
  export { AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, type CloudflareBootstrapServices as BootstrapServices, Class, ExplainedRecovery, type CloudflareDurableRuntimeInitializationError as InitializationError, Instance, ObligationsScanned, Options, type ThreadPublicationOptions as PublicationOptions, RetryExecuted, type CloudflareDurableRuntimeOptions as RuntimeOptions, type CloudflareDurableRuntimeServices as Services, ThreadObjectPorts, ThreadRpcOperation, VerifiedIntegrity, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeObligationThresholds, decodeRetryCommand, encodeAdminResponse, handleRpc, layer, layerConfig, layerHostConfig, layerInHost, make, portCall, submit };
package/dist/index.d.mts CHANGED
@@ -8,6 +8,6 @@ import { t as CloudflareMemory_d_exports } from "./CloudflareMemory.mjs";
8
8
  import { t as CloudflareScheduling_d_exports } from "./CloudflareScheduling.mjs";
9
9
  import { t as CloudflareSubscriptions_d_exports } from "./CloudflareSubscriptions.mjs";
10
10
  import { t as CloudflareThreadClient_d_exports } from "./CloudflareThreadClient.mjs";
11
- import { f as ThreadObject_d_exports } from "./ThreadObject-CnqCuWlA.mjs";
11
+ import { f as ThreadObject_d_exports } from "./ThreadObject-D_vvj7Qx.mjs";
12
12
  import { t as WakeScheduler_d_exports } from "./WakeScheduler.mjs";
13
13
  export { Alarm_d_exports as Alarm, CloudflareAiGateway_d_exports as CloudflareAiGateway, CloudflareBindings_d_exports as CloudflareBindings, CloudflareBrowser_d_exports as CloudflareBrowser, CloudflareCodeMode_d_exports as CloudflareCodeMode, CloudflareConfig_d_exports as CloudflareConfig, CloudflareMemory_d_exports as CloudflareMemory, CloudflareScheduling_d_exports as CloudflareScheduling, CloudflareSubscriptions_d_exports as CloudflareSubscriptions, CloudflareThreadClient_d_exports as CloudflareThreadClient, ThreadObject_d_exports as ThreadObject, WakeScheduler_d_exports as WakeScheduler };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.107","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.107","@effect/platform-browser":"4.0.0-rc.115","@effect/sql-sqlite-do":"4.0.0-rc.115","effect-agent":"0.1.0-beta.107"},"devDependencies":{"@cloudflare/puppeteer":"1.1.0","@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.107","@effect/platform-node":"4.0.0-rc.115","@effect/sql-d1":"4.0.0-rc.115","@effect/vitest":"4.0.0-rc.115","effect":"4.0.0-rc.115","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"@cloudflare/puppeteer":"^1.1.0","effect":"^4.0.0-rc.115","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./protected-browser":{"types":"./dist/ProtectedBrowser.d.mts","default":"./dist/ProtectedBrowser.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"},"peerDependenciesMeta":{"@cloudflare/puppeteer":{"optional":true}}}
1
+ {"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.108","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.108","@effect/platform-browser":"4.0.0-rc.115","@effect/sql-sqlite-do":"4.0.0-rc.115","effect-agent":"0.1.0-beta.108"},"devDependencies":{"@cloudflare/puppeteer":"1.1.0","@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.108","@effect/platform-node":"4.0.0-rc.115","@effect/sql-d1":"4.0.0-rc.115","@effect/vitest":"4.0.0-rc.115","effect":"4.0.0-rc.115","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"@cloudflare/puppeteer":"^1.1.0","effect":"^4.0.0-rc.115","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./protected-browser":{"types":"./dist/ProtectedBrowser.d.mts","default":"./dist/ProtectedBrowser.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"},"peerDependenciesMeta":{"@cloudflare/puppeteer":{"optional":true}}}
@@ -1,6 +1,6 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
2
 
3
- import puppeteer, {
3
+ import {
4
4
  type Browser,
5
5
  type BrowserContext,
6
6
  type CDPSession,
@@ -8,6 +8,7 @@ import puppeteer, {
8
8
  type Page,
9
9
  } from "@cloudflare/puppeteer";
10
10
  import {
11
+ Cause,
11
12
  Context,
12
13
  Clock,
13
14
  Duration,
@@ -45,6 +46,12 @@ import {
45
46
  import { PageScreenshotResult } from "effect-agent/page-screenshot";
46
47
  import { SandboxImplementation } from "effect-agent/sandbox";
47
48
 
49
+ import { BrowserRunBinding } from "./internal/browser-binding.ts";
50
+ import {
51
+ BrowserRunFailure,
52
+ reportBrowserCause,
53
+ reportedBrowserError,
54
+ } from "./internal/browser-failure.ts";
48
55
  import { makeFileSelection } from "./internal/browser-file-selection.ts";
49
56
  import {
50
57
  BrowserRunSessionLifecycle,
@@ -379,22 +386,26 @@ export class BrowserRunInteractiveBinding extends Context.Service<
379
386
  return Layer.effect(BrowserRunInteractiveBinding)(
380
387
  Effect.gen(function* () {
381
388
  const lifecycle = yield* BrowserRunSessionLifecycle;
389
+ const binding = yield* BrowserRunBinding;
382
390
 
383
391
  const viewport =
384
392
  options.viewport === undefined ? undefined : yield* decodeViewport(options.viewport);
385
393
 
386
394
  return {
387
395
  acquire: async (keepAliveMillis: number) =>
388
- (await puppeteer.acquire(options.browser, { keep_alive: keepAliveMillis })).sessionId,
396
+ binding.acquire(keepAliveMillis, "interactive.acquire"),
389
397
  connect: async (sessionId: string) =>
390
- makeProductionBrowser(await puppeteer.connect(options.browser, sessionId), viewport),
398
+ makeProductionBrowser(
399
+ await binding.connect(sessionId, "interactive.connect"),
400
+ viewport,
401
+ ),
391
402
  closeSession: (sessionId: Redacted.Redacted<string>) =>
392
403
  lifecycle
393
404
  .close(sessionId)
394
405
  .pipe(Effect.mapError((cause) => actionError("close", cause))),
395
406
  };
396
407
  }),
397
- );
408
+ ).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
398
409
  }
399
410
  }
400
411
 
@@ -776,7 +787,14 @@ const makeProductionPage = (page: Page): BrowserRunInteractivePage => {
776
787
  const listeners = new Map<BrowserRunInteractiveRequestListener, (request: HTTPRequest) => void>();
777
788
 
778
789
  return {
779
- close: () => page.close(),
790
+ close: async () => {
791
+ if (!page.browser().isConnected()) return;
792
+ try {
793
+ await page.close();
794
+ } catch (cause) {
795
+ if (!isRemoteClosure(cause)) throw cause;
796
+ }
797
+ },
780
798
  setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
781
799
  setRequestInterception: (enabled) => page.setRequestInterception(enabled),
782
800
  onRequest: (listener) => {
@@ -1144,7 +1162,14 @@ const makeProductionContext = (
1144
1162
 
1145
1163
  return makeProductionPage(page);
1146
1164
  },
1147
- close: () => context.close(),
1165
+ close: async () => {
1166
+ if (!context.browser().isConnected()) return;
1167
+ try {
1168
+ await context.close();
1169
+ } catch (cause) {
1170
+ if (!isRemoteClosure(cause)) throw cause;
1171
+ }
1172
+ },
1148
1173
  });
1149
1174
 
1150
1175
  const makeProductionBrowser = (
@@ -1152,7 +1177,15 @@ const makeProductionBrowser = (
1152
1177
  viewport?: BrowserRunViewport,
1153
1178
  ): BrowserRunInteractiveBrowser => ({
1154
1179
  createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
1155
- close: () => browser.close(),
1180
+ // Exact-session termination may already have closed the provider transport.
1181
+ close: async () => {
1182
+ if (!browser.isConnected()) return;
1183
+ try {
1184
+ await browser.close();
1185
+ } catch (cause) {
1186
+ if (!isRemoteClosure(cause)) throw cause;
1187
+ }
1188
+ },
1156
1189
  sessionId: () => browser.sessionId(),
1157
1190
  isConnected: () => browser.isConnected(),
1158
1191
  onDisconnected: (listener) => {
@@ -1211,7 +1244,7 @@ const causeText = (cause: unknown): string => {
1211
1244
  };
1212
1245
 
1213
1246
  const isCapacityRefusal = (cause: unknown): boolean =>
1214
- /(^|\D)429(\D|$)|browser time limit|capacity|too many concurrent/i.test(causeText(cause));
1247
+ cause instanceof BrowserRunFailure && cause.status === 429;
1215
1248
 
1216
1249
  const isRemoteClosure = (cause: unknown): boolean =>
1217
1250
  /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(
@@ -1270,6 +1303,8 @@ const hostAllowed = (policy: InteractiveBrowserPolicySnapshot, value: string): b
1270
1303
  const keepAliveMillis = (policy: InteractiveBrowserPolicySnapshot): number =>
1271
1304
  Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
1272
1305
 
1306
+ class LateBrowserAcquisition extends Error {}
1307
+
1273
1308
  const closeLateAcquisition = async <A>(
1274
1309
  signal: AbortSignal,
1275
1310
  acquire: () => Promise<A>,
@@ -1284,22 +1319,25 @@ const closeLateAcquisition = async <A>(
1284
1319
  try {
1285
1320
  await close(acquired);
1286
1321
  } catch {
1287
- // No caller remains to observe a late cleanup failure, and provider details
1288
- // must not escape through an unhandled rejection.
1322
+ // The captured runtime reports cleanup failures before this callback absorbs rejection.
1289
1323
  }
1290
- throw new Error("The interrupted browser acquisition completed late");
1324
+ throw new LateBrowserAcquisition("The interrupted browser acquisition completed late");
1291
1325
  };
1292
1326
 
1293
1327
  const closeWithWarning = (close: () => Promise<void>, warning: string): Effect.Effect<void> =>
1294
1328
  Effect.tryPromise({
1295
1329
  try: close,
1296
- catch: () => protocolError(warning),
1330
+ catch: (cause) => protocolError(warning, cause),
1297
1331
  }).pipe(
1298
1332
  Effect.timeoutOrElse({
1299
1333
  duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
1300
1334
  orElse: () => Effect.fail(protocolError(warning)),
1301
1335
  }),
1302
- Effect.catchCause(() => Effect.logWarning(warning)),
1336
+ Effect.catchCause((cause) =>
1337
+ reportBrowserCause("interactive.cleanup", cause).pipe(
1338
+ Effect.andThen(Effect.logWarning(warning)),
1339
+ ),
1340
+ ),
1303
1341
  );
1304
1342
 
1305
1343
  const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (
@@ -1459,15 +1497,21 @@ const makeRequestListener =
1459
1497
 
1460
1498
  try {
1461
1499
  settlement = allowed ? request.continue() : request.abort();
1462
- } catch {
1463
- state.violation.value = protocolError("Resolving an intercepted browser request failed");
1500
+ } catch (cause) {
1501
+ state.violation.value = protocolError(
1502
+ "Resolving an intercepted browser request failed",
1503
+ cause,
1504
+ );
1464
1505
 
1465
1506
  return;
1466
1507
  }
1467
1508
 
1468
1509
  const observed = settlement
1469
- .catch(() => {
1470
- state.violation.value = protocolError("Resolving an intercepted browser request failed");
1510
+ .catch((cause) => {
1511
+ state.violation.value = protocolError(
1512
+ "Resolving an intercepted browser request failed",
1513
+ cause,
1514
+ );
1471
1515
  })
1472
1516
  .finally(() => {
1473
1517
  state.pendingRequests.delete(observed);
@@ -1923,10 +1967,31 @@ const cdpCommand = <A>(
1923
1967
  ): Effect.Effect<A, InteractiveBrowserError> =>
1924
1968
  Effect.scoped(
1925
1969
  Effect.gen(function* () {
1970
+ const cleanupContext = yield* Effect.context<never>();
1971
+
1926
1972
  const cdp = yield* Effect.acquireRelease(
1927
1973
  Effect.tryPromise({
1928
- try: (signal) =>
1929
- closeLateAcquisition(signal, page.createCdpSession, (acquired) => acquired.detach()),
1974
+ try: (signal) => {
1975
+ const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) =>
1976
+ Effect.runPromiseWith(cleanupContext)(
1977
+ Effect.tryPromise({
1978
+ try: () => acquired.detach(),
1979
+ catch: (cause) => actionError("close", cause),
1980
+ }).pipe(
1981
+ Effect.tapCause((cause) => reportBrowserCause("interactive.lateDetach", cause)),
1982
+ ),
1983
+ ),
1984
+ );
1985
+
1986
+ void pending.then(undefined, (cause) => {
1987
+ if (signal.aborted && !(cause instanceof LateBrowserAcquisition))
1988
+ return Effect.runPromiseWith(cleanupContext)(
1989
+ reportBrowserCause("interactive.connect", Cause.fail(cause)),
1990
+ );
1991
+ });
1992
+
1993
+ return pending;
1994
+ },
1930
1995
  catch: (cause) =>
1931
1996
  state.disconnected.value || isRemoteClosure(cause)
1932
1997
  ? expiredError()
@@ -1949,7 +2014,11 @@ const cdpCommand = <A>(
1949
2014
  });
1950
2015
 
1951
2016
  return yield* Schema.decodeUnknownEffect(output)(raw).pipe(
1952
- Effect.mapError(() => protocolError(malformedMessage)),
2017
+ Effect.catch((error) =>
2018
+ reportBrowserCause("interactive.command.decode", Cause.fail(error)).pipe(
2019
+ Effect.andThen(Effect.fail(reportedBrowserError(protocolError(malformedMessage)))),
2020
+ ),
2021
+ ),
1953
2022
  );
1954
2023
  }),
1955
2024
  );
@@ -1981,14 +2050,25 @@ const makeHostService = (
1981
2050
  // Remote termination is authoritative. Local cleanup must not veto it or extend the deadline.
1982
2051
  yield* runTeardown(entries).pipe(
1983
2052
  Effect.flatMap((failures) =>
1984
- Effect.forEach(failures, (failure) => Effect.logWarning(failure.warning), {
1985
- discard: true,
1986
- }),
2053
+ Effect.forEach(
2054
+ failures,
2055
+ (failure) =>
2056
+ reportBrowserCause("interactive.cleanup", Cause.fail(failure.error)).pipe(
2057
+ Effect.andThen(Effect.logWarning(failure.warning)),
2058
+ ),
2059
+ {
2060
+ discard: true,
2061
+ },
2062
+ ),
1987
2063
  ),
1988
2064
  Effect.interruptible,
1989
2065
  Effect.timeout(`${remaining} millis`),
1990
- Effect.catchCause(() =>
1991
- Effect.logWarning("Local browser teardown incomplete after confirmed termination"),
2066
+ Effect.catchCause((cause) =>
2067
+ reportBrowserCause("interactive.cleanup", cause).pipe(
2068
+ Effect.andThen(
2069
+ Effect.logWarning("Local browser teardown incomplete after confirmed termination"),
2070
+ ),
2071
+ ),
1992
2072
  ),
1993
2073
  );
1994
2074
  });
@@ -1998,10 +2078,10 @@ const makeHostService = (
1998
2078
  ) {
1999
2079
  const sessionId = yield* Effect.try({
2000
2080
  try: browser.sessionId,
2001
- catch: () => actionError("close"),
2081
+ catch: (cause) => actionError("close", cause),
2002
2082
  }).pipe(
2003
2083
  Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)),
2004
- Effect.mapError(() => actionError("close")),
2084
+ Effect.mapError((cause) => actionError("close", cause)),
2005
2085
  Effect.onError(() =>
2006
2086
  closeWithWarning(browser.close, "Closing an unidentified browser failed"),
2007
2087
  ),
@@ -2018,8 +2098,26 @@ const makeHostService = (
2018
2098
  const scope = yield* Scope.Scope;
2019
2099
  const fixedPolicy = yield* snapshotPolicy(policy);
2020
2100
  const startedAt = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
2021
- // Late SDK replies outlive the opening fiber; cleanup retains this pass's clock.
2022
- const runCleanup = Effect.runPromiseWith(Context.make(Clock.Clock, yield* Clock.Clock));
2101
+
2102
+ // Late SDK replies retain this pass's clock and private reporting scope.
2103
+ const runCleanup = <A, E>(effect: Effect.Effect<A, E>) =>
2104
+ Effect.runPromiseWith(cleanupContext)(
2105
+ effect.pipe(
2106
+ Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause)),
2107
+ ),
2108
+ );
2109
+
2110
+ const cleanupContext = yield* Effect.context<never>();
2111
+
2112
+ const observeLate = <A>(signal: AbortSignal, pending: Promise<A>): Promise<A> => {
2113
+ // Observe the original Promise without creating another rejected continuation.
2114
+ void pending.then(undefined, (cause) => {
2115
+ if (signal.aborted && !(cause instanceof LateBrowserAcquisition))
2116
+ return runCleanup(reportBrowserCause("interactive.acquire", Cause.fail(cause)));
2117
+ });
2118
+
2119
+ return pending;
2120
+ };
2023
2121
 
2024
2122
  const state: HandleState = {
2025
2123
  closed: { value: false },
@@ -2039,22 +2137,31 @@ const makeHostService = (
2039
2137
  Effect.suspend(() =>
2040
2138
  lifecycle.managedTeardownInstalled
2041
2139
  ? Effect.void
2042
- : entry.close.pipe(Effect.catchCause(() => Effect.logWarning(entry.warning))),
2140
+ : entry.close.pipe(
2141
+ Effect.catchCause((cause) =>
2142
+ reportBrowserCause("interactive.cleanup", cause).pipe(
2143
+ Effect.andThen(Effect.logWarning(entry.warning)),
2144
+ ),
2145
+ ),
2146
+ ),
2043
2147
  );
2044
2148
 
2045
2149
  const sessionIdValue = yield* Effect.acquireRelease(
2046
2150
  withinDeadline(
2047
2151
  Effect.tryPromise({
2048
2152
  try: (signal) =>
2049
- closeLateAcquisition(
2153
+ observeLate(
2050
2154
  signal,
2051
- () => binding.acquire(keepAliveMillis(fixedPolicy)),
2052
- (id) =>
2053
- runCleanup(
2054
- Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(
2055
- Effect.flatMap((value) => terminate(Redacted.make(value), [])),
2155
+ closeLateAcquisition(
2156
+ signal,
2157
+ () => binding.acquire(keepAliveMillis(fixedPolicy)),
2158
+ (id) =>
2159
+ runCleanup(
2160
+ Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(
2161
+ Effect.flatMap((value) => terminate(Redacted.make(value), [])),
2162
+ ),
2056
2163
  ),
2057
- ),
2164
+ ),
2058
2165
  ),
2059
2166
  catch: (cause) =>
2060
2167
  isCapacityRefusal(cause)
@@ -2080,7 +2187,11 @@ const makeHostService = (
2080
2187
  return lifecycle.managedTeardownInstalled
2081
2188
  ? Effect.void
2082
2189
  : terminate(Redacted.make(id), []).pipe(
2083
- Effect.catch(() => Effect.logWarning("Whole-browser cleanup remains unconfirmed")),
2190
+ Effect.catchCause((cause) =>
2191
+ reportBrowserCause("interactive.close", cause).pipe(
2192
+ Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")),
2193
+ ),
2194
+ ),
2084
2195
  );
2085
2196
  }),
2086
2197
  { interruptible: true },
@@ -2096,10 +2207,13 @@ const makeHostService = (
2096
2207
  withinDeadline(
2097
2208
  Effect.tryPromise({
2098
2209
  try: (signal) =>
2099
- closeLateAcquisition(
2210
+ observeLate(
2100
2211
  signal,
2101
- () => binding.connect(sessionIdValue),
2102
- (acquired) => runCleanup(closeAcquired(acquired)),
2212
+ closeLateAcquisition(
2213
+ signal,
2214
+ () => binding.connect(sessionIdValue),
2215
+ (acquired) => runCleanup(closeAcquired(acquired)),
2216
+ ),
2103
2217
  ),
2104
2218
  catch: (cause) =>
2105
2219
  protocolError("Connecting to the Browser Run session failed", cause),
@@ -2156,7 +2270,17 @@ const makeHostService = (
2156
2270
  withinDeadline(
2157
2271
  Effect.tryPromise({
2158
2272
  try: (signal) =>
2159
- closeLateAcquisition(signal, browser.createContext, (acquired) => acquired.close()),
2273
+ observeLate(
2274
+ signal,
2275
+ closeLateAcquisition(signal, browser.createContext, (acquired) =>
2276
+ runCleanup(
2277
+ Effect.tryPromise({
2278
+ try: () => acquired.close(),
2279
+ catch: (cause) => actionError("close", cause),
2280
+ }),
2281
+ ),
2282
+ ),
2283
+ ),
2160
2284
  catch: (cause) =>
2161
2285
  state.disconnected.value || isRemoteClosure(cause)
2162
2286
  ? expiredError()
@@ -2178,7 +2302,17 @@ const makeHostService = (
2178
2302
  withinDeadline(
2179
2303
  Effect.tryPromise({
2180
2304
  try: (signal) =>
2181
- closeLateAcquisition(signal, context.newPage, (acquired) => acquired.close()),
2305
+ observeLate(
2306
+ signal,
2307
+ closeLateAcquisition(signal, context.newPage, (acquired) =>
2308
+ runCleanup(
2309
+ Effect.tryPromise({
2310
+ try: () => acquired.close(),
2311
+ catch: (cause) => actionError("close", cause),
2312
+ }),
2313
+ ),
2314
+ ),
2315
+ ),
2182
2316
  catch: (cause) =>
2183
2317
  state.disconnected.value || isRemoteClosure(cause)
2184
2318
  ? expiredError()
@@ -2244,7 +2378,12 @@ const makeHostService = (
2244
2378
 
2245
2379
  const teardown = yield* Effect.uninterruptible(
2246
2380
  Effect.gen(function* () {
2247
- const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers));
2381
+ const cached = yield* Effect.cached(
2382
+ terminate(Redacted.make(sessionIdValue), closers).pipe(
2383
+ Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)),
2384
+ Effect.mapError(reportedBrowserError),
2385
+ ),
2386
+ );
2248
2387
 
2249
2388
  lifecycle.managedTeardownInstalled = true;
2250
2389
  yield* Effect.addFinalizer(() =>
@@ -2254,8 +2393,12 @@ const makeHostService = (
2254
2393
  state.disconnected.value = true;
2255
2394
  }).pipe(
2256
2395
  Effect.andThen(cached),
2257
- Effect.catch(() =>
2258
- Effect.logWarning("Whole-browser cleanup remains unconfirmed"),
2396
+ Effect.catchCause((cause) =>
2397
+ reportBrowserCause("interactive.close", cause).pipe(
2398
+ Effect.andThen(
2399
+ Effect.logWarning("Whole-browser cleanup remains unconfirmed"),
2400
+ ),
2401
+ ),
2259
2402
  ),
2260
2403
  ),
2261
2404
  ),