@yielded/agent-platform-cloudflare 0.1.0-beta.167 → 0.1.0-beta.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/{Alarm-B0ODr1NE.mjs → Alarm-DxODgAZZ.mjs} +259 -159
  2. package/dist/Alarm-DxODgAZZ.mjs.map +1 -0
  3. package/dist/Alarm.d.mts +27 -36
  4. package/dist/Alarm.mjs +2 -2
  5. package/dist/BrowserCredentials-C5IWfi7f.d.mts +91 -0
  6. package/dist/BrowserCredentials.d.mts +2 -91
  7. package/dist/BrowserRestCapture.mjs +3 -3
  8. package/dist/BrowserRestCapture.mjs.map +1 -1
  9. package/dist/BrowserRestCrawl.mjs +6 -6
  10. package/dist/BrowserRestCrawl.mjs.map +1 -1
  11. package/dist/{BrowserSession-RfIzxGCH.mjs → BrowserSession-BfNmyayH.mjs} +5 -5
  12. package/dist/BrowserSession-BfNmyayH.mjs.map +1 -0
  13. package/dist/{BrowserSession-B969Xg-2.d.mts → BrowserSession-Bx0V1-6_.d.mts} +6 -3
  14. package/dist/BrowserSession.d.mts +1 -1
  15. package/dist/BrowserSession.mjs +2 -2
  16. package/dist/BrowserUse-AstK0jKX.mjs +1467 -0
  17. package/dist/BrowserUse-AstK0jKX.mjs.map +1 -0
  18. package/dist/BrowserUse.d.mts +83 -0
  19. package/dist/BrowserUse.mjs +2 -0
  20. package/dist/CloudflareBindings.d.mts +8 -1
  21. package/dist/CloudflareBindings.mjs +2 -2
  22. package/dist/CloudflareBindings.mjs.map +1 -1
  23. package/dist/{CloudflareBrowser-5an6m-Qb.mjs → CloudflareBrowser-Vt8qTdYW.mjs} +11 -13
  24. package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -0
  25. package/dist/CloudflareBrowser.mjs +1 -1
  26. package/dist/CloudflareCodeMode.mjs +1 -1
  27. package/dist/CloudflareCodeMode.mjs.map +1 -1
  28. package/dist/{CloudflareConfig-qDrRLofL.d.mts → CloudflareConfig-C9MvPbX8.d.mts} +9 -11
  29. package/dist/CloudflareConfig.d.mts +1 -1
  30. package/dist/CloudflareConfig.mjs +9 -12
  31. package/dist/CloudflareConfig.mjs.map +1 -1
  32. package/dist/CloudflareMemory.d.mts +3 -25
  33. package/dist/CloudflareMemory.mjs +33 -31
  34. package/dist/CloudflareMemory.mjs.map +1 -1
  35. package/dist/CloudflareScheduling.mjs +8 -15
  36. package/dist/CloudflareScheduling.mjs.map +1 -1
  37. package/dist/CloudflareSubscriptions.mjs +9 -16
  38. package/dist/CloudflareSubscriptions.mjs.map +1 -1
  39. package/dist/CloudflareThreadClient-CZm0TvUe.mjs +588 -0
  40. package/dist/CloudflareThreadClient-CZm0TvUe.mjs.map +1 -0
  41. package/dist/CloudflareThreadClient-QsVzLGGZ.d.mts +2033 -0
  42. package/dist/CloudflareThreadClient.d.mts +2 -1796
  43. package/dist/CloudflareThreadClient.mjs +2 -382
  44. package/dist/{InteractiveBrowser-BbG3kGA9.mjs → InteractiveBrowser-LdKzQeLR.mjs} +18 -19
  45. package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +1 -0
  46. package/dist/InteractiveBrowser.mjs +1 -1
  47. package/dist/{ThreadObject-BcfXbzSO.d.mts → ThreadObject-7s5XnlH-.d.mts} +103 -79
  48. package/dist/{ThreadObject-DHjrnlX4.mjs → ThreadObject-C0vrcDrw.mjs} +265 -66
  49. package/dist/ThreadObject-C0vrcDrw.mjs.map +1 -0
  50. package/dist/ThreadObject.d.mts +2 -2
  51. package/dist/ThreadObject.mjs +2 -3
  52. package/dist/WakeScheduler.d.mts +6 -5
  53. package/dist/WakeScheduler.mjs +15 -11
  54. package/dist/WakeScheduler.mjs.map +1 -1
  55. package/dist/index.d.mts +7 -6
  56. package/dist/index.mjs +7 -6
  57. package/dist/page-function-CmUCcyNP.mjs +11 -0
  58. package/dist/page-function-CmUCcyNP.mjs.map +1 -0
  59. package/dist/{prepared-admission-DMX5p9dC.mjs → prepared-admission-3UUq0zth.mjs} +3 -3
  60. package/dist/{prepared-admission-DMX5p9dC.mjs.map → prepared-admission-3UUq0zth.mjs.map} +1 -1
  61. package/package.json +1 -1
  62. package/src/Alarm.ts +510 -279
  63. package/src/BrowserRestCapture.ts +2 -2
  64. package/src/BrowserRestCrawl.ts +10 -14
  65. package/src/BrowserSession.ts +10 -2
  66. package/src/BrowserUse.ts +1541 -0
  67. package/src/CloudflareBindings.ts +13 -4
  68. package/src/CloudflareCodeMode.ts +1 -1
  69. package/src/CloudflareConfig.ts +9 -12
  70. package/src/CloudflareMemory.ts +21 -28
  71. package/src/CloudflareScheduling.ts +7 -15
  72. package/src/CloudflareSubscriptions.ts +11 -20
  73. package/src/CloudflareThreadClient.ts +289 -9
  74. package/src/InteractiveBrowser.ts +325 -323
  75. package/src/ThreadObject.ts +172 -52
  76. package/src/WakeScheduler.ts +29 -18
  77. package/src/index.ts +1 -0
  78. package/src/internal/browser-dom.ts +845 -0
  79. package/src/internal/browser-file-selection.ts +8 -1
  80. package/src/internal/browser-jev-dom.ts +344 -0
  81. package/src/internal/browser-quick-action.ts +7 -8
  82. package/src/internal/browser-session-lifecycle.ts +2 -5
  83. package/src/internal/crypto.ts +41 -0
  84. package/src/internal/due-queue.ts +5 -69
  85. package/src/internal/layers.ts +100 -17
  86. package/src/internal/live-text-protocol.ts +51 -0
  87. package/src/internal/live-text.ts +256 -0
  88. package/src/internal/page-function.ts +12 -0
  89. package/src/internal/prepared-admission.ts +1 -1
  90. package/src/internal/progress-wait.ts +19 -24
  91. package/src/internal/transport.ts +1 -1
  92. package/dist/Alarm-B0ODr1NE.mjs.map +0 -1
  93. package/dist/BrowserSession-RfIzxGCH.mjs.map +0 -1
  94. package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +0 -1
  95. package/dist/CloudflareThreadClient.mjs.map +0 -1
  96. package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +0 -1
  97. package/dist/ThreadObject-DHjrnlX4.mjs.map +0 -1
@@ -1,4 +1,3 @@
1
- import { BrowserCrypto } from "@effect/platform-browser";
2
1
  import { AgentInputError } from "@yielded/agent/agent-error";
3
2
  import { DigestError } from "@yielded/agent/digest";
4
3
  import {
@@ -15,6 +14,7 @@ import {
15
14
  CanonicalSequence,
16
15
  DefinitionDigests,
17
16
  PersistedJson,
17
+ SubmissionSettledRecord,
18
18
  WorkerAdmission,
19
19
  } from "@yielded/agent/records";
20
20
  import {
@@ -30,6 +30,7 @@ import {
30
30
  IdempotencyKey,
31
31
  JoinedToHost,
32
32
  LedgerError,
33
+ OwnershipLost,
33
34
  Principal,
34
35
  Settlement,
35
36
  SettlementConflict,
@@ -42,10 +43,9 @@ import {
42
43
  AppendConflict,
43
44
  ThreadNotMaterialized,
44
45
  ThreadStoreError,
45
- FenceRejected,
46
46
  } from "@yielded/agent/thread-store";
47
- import { Context, Crypto, Duration, Effect, Layer, Schema } from "effect";
48
- import { RpcTracing } from "effect-cf";
47
+ import { Context, Crypto, Duration, Effect, Layer, Predicate, Schema, Stream } from "effect";
48
+ import { RpcTargets, RpcTracing } from "effect-cf";
49
49
 
50
50
  import { DurableAlarmError } from "./Alarm.ts";
51
51
  import {
@@ -55,6 +55,16 @@ import {
55
55
  } from "./CloudflareBindings.ts";
56
56
  import { AdmissionLimitExceeded } from "./CloudflareConfig.ts";
57
57
  import { cloudflareFailureSignals, safeCauseMessage } from "./internal/boundary.ts";
58
+ import { cloudflareCryptoLayer } from "./internal/crypto.ts";
59
+ import {
60
+ Frame as LiveTextFrame,
61
+ isBoundedEvent,
62
+ MAX_DRAFT_PARTS,
63
+ MAX_FRAME_BYTES,
64
+ Request as WatchTextRequest,
65
+ } from "./internal/live-text-protocol.ts";
66
+
67
+ export { Frame as LiveTextFrame } from "./internal/live-text-protocol.ts";
58
68
 
59
69
  /**
60
70
  * The Worker↔Thread-Object host protocol (plan §1.4): Schema envelopes for the host
@@ -169,10 +179,10 @@ export const HostFailure = Schema.Union([
169
179
  UnknownResolutionConflict,
170
180
  JoinedToHost,
171
181
  LedgerError,
182
+ OwnershipLost,
172
183
  ThreadStoreError,
173
184
  ThreadNotMaterialized,
174
185
  AppendConflict,
175
- FenceRejected,
176
186
  DurableRuntimeFailpointError,
177
187
  AdmissionLimitExceeded,
178
188
  DurableAlarmError,
@@ -198,6 +208,13 @@ export class SettlementReached extends Schema.TaggedClass<SettlementReached>(
198
208
  settlement: Settlement,
199
209
  }) {}
200
210
 
211
+ /** The exact canonical record of an operationally finalized Submission. */
212
+ export class SettlementRecordReached extends Schema.TaggedClass<SettlementRecordReached>(
213
+ "@effect-agent/platform-cloudflare/SettlementRecordReached",
214
+ )("SettlementRecordReached", {
215
+ record: SubmissionSettledRecord,
216
+ }) {}
217
+
201
218
  export class ObservedPage extends Schema.TaggedClass<ObservedPage>(
202
219
  "@effect-agent/platform-cloudflare/ObservedPage",
203
220
  )("ObservedPage", {
@@ -242,6 +259,7 @@ export class HostFailed extends Schema.TaggedClass<HostFailed>(
242
259
  export const HostResponse = Schema.Union([
243
260
  SubmitSucceeded,
244
261
  SettlementReached,
262
+ SettlementRecordReached,
245
263
  SubmissionStatusResponse,
246
264
  ObservedPage,
247
265
  ProgressObserved,
@@ -288,10 +306,10 @@ const ClientSubmitHostFailure = Schema.Union([
288
306
  AdmissionConflict,
289
307
  AdmissionPolicyError,
290
308
  LedgerError,
309
+ OwnershipLost,
291
310
  ThreadStoreError,
292
311
  ThreadNotMaterialized,
293
312
  AppendConflict,
294
- FenceRejected,
295
313
  DurableRuntimeFailpointError,
296
314
  AdmissionLimitExceeded,
297
315
  DurableAlarmError,
@@ -300,6 +318,7 @@ const ClientSubmitHostFailure = Schema.Union([
300
318
 
301
319
  const ClientAwaitHostFailure = Schema.Union([
302
320
  LedgerError,
321
+ OwnershipLost,
303
322
  SettlementConflict,
304
323
  OperationDenied,
305
324
  HostProtocolError,
@@ -315,6 +334,7 @@ const ClientObserveHostFailure = Schema.Union([
315
334
  const ClientAbortHostFailure = Schema.Union([
316
335
  OperationDenied,
317
336
  LedgerError,
337
+ OwnershipLost,
318
338
  SettlementConflict,
319
339
  JoinedToHost,
320
340
  DurableRuntimeFailpointError,
@@ -324,6 +344,7 @@ const ClientAbortHostFailure = Schema.Union([
324
344
 
325
345
  const ClientApprovalHostFailure = Schema.Union([
326
346
  LedgerError,
347
+ OwnershipLost,
327
348
  SettlementConflict,
328
349
  ApprovalConflict,
329
350
  OperationDenied,
@@ -333,6 +354,7 @@ const ClientApprovalHostFailure = Schema.Union([
333
354
 
334
355
  const ClientUnknownHostFailure = Schema.Union([
335
356
  LedgerError,
357
+ OwnershipLost,
336
358
  SettlementConflict,
337
359
  UnknownResolutionConflict,
338
360
  JoinedToHost,
@@ -350,6 +372,127 @@ export type ClientAbortFailure = typeof ClientAbortHostFailure.Type | ThreadClie
350
372
  export type ClientApprovalFailure = typeof ClientApprovalHostFailure.Type | ThreadClientError;
351
373
  export type ClientUnknownFailure = typeof ClientUnknownHostFailure.Type | ThreadClientError;
352
374
 
375
+ const liveTextProtocolFailure = () =>
376
+ HostProtocolError.make({
377
+ message: "Provisional text is incomplete or invalid; discard drafts and read canonical records",
378
+ });
379
+
380
+ const decodeLiveText = (
381
+ threadId: ThreadId,
382
+ source: ReadableStream<unknown>,
383
+ ): Stream.Stream<LiveTextFrame, HostProtocolError | ThreadClientError> =>
384
+ Stream.suspend(() => {
385
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
386
+ const decode = Schema.decodeUnknownSync(Schema.fromJsonString(LiveTextFrame));
387
+ let pending = "";
388
+ let pendingBytes = 0;
389
+ let expected = 0;
390
+ let streamId: string | undefined;
391
+ const models = new Map<string, { attemptId: string; parts: Set<string> }>();
392
+
393
+ const accept = (frame: LiveTextFrame): boolean => {
394
+ if (frame._tag === "Reset") {
395
+ if (frame.streamId === streamId || frame.threadId !== threadId)
396
+ throw liveTextProtocolFailure();
397
+ streamId = frame.streamId;
398
+ expected = 1;
399
+ models.clear();
400
+
401
+ return true;
402
+ }
403
+ if (
404
+ streamId === undefined ||
405
+ frame.streamId !== streamId ||
406
+ frame.sequence !== expected++ ||
407
+ frame.event.threadId !== threadId ||
408
+ !isBoundedEvent(frame.event)
409
+ )
410
+ throw liveTextProtocolFailure();
411
+ const event = frame.event;
412
+
413
+ if (event._tag === "AttemptEnded") {
414
+ for (const [key, model] of models)
415
+ if (model.attemptId === event.attemptId) models.delete(key);
416
+
417
+ return true;
418
+ }
419
+ const key = JSON.stringify([event.attemptId, event.runId, event.turnId, event.generation]);
420
+
421
+ if (event._tag === "Discard") {
422
+ models.delete(key);
423
+
424
+ return true;
425
+ }
426
+ let model = models.get(key);
427
+
428
+ if (event.part.type === "text-start") {
429
+ if (model?.parts.has(event.part.id)) throw liveTextProtocolFailure();
430
+ if (
431
+ [...models.values()].reduce((size, value) => size + value.parts.size, 0) >=
432
+ MAX_DRAFT_PARTS
433
+ )
434
+ throw liveTextProtocolFailure();
435
+ if (model === undefined) {
436
+ model = { attemptId: event.attemptId, parts: new Set() };
437
+ models.set(key, model);
438
+ }
439
+ model.parts.add(event.part.id);
440
+
441
+ return true;
442
+ }
443
+ if (!model?.parts.has(event.part.id)) throw liveTextProtocolFailure();
444
+ if (event.part.type === "text-end") {
445
+ model.parts.delete(event.part.id);
446
+ if (model.parts.size === 0) models.delete(key);
447
+ }
448
+
449
+ return true;
450
+ };
451
+
452
+ return Stream.fromReadableStream({
453
+ evaluate: () => source,
454
+ // The subscription Scope owns the single bounded cancellation after this reader unlocks.
455
+ releaseLockOnEnd: true,
456
+ onError: (cause) =>
457
+ ThreadClientError.make({
458
+ threadId,
459
+ message: "Provisional text disconnected; discard drafts and read canonical records",
460
+ cause,
461
+ }),
462
+ }).pipe(
463
+ Stream.mapEffect((chunk) =>
464
+ Effect.try({
465
+ try: () => {
466
+ if (!(chunk instanceof Uint8Array)) throw liveTextProtocolFailure();
467
+ const frames: LiveTextFrame[] = [];
468
+
469
+ for (let offset = 0; offset < chunk.byteLength;) {
470
+ const newline = chunk.indexOf(10, offset);
471
+ const end = newline < 0 ? chunk.byteLength : newline;
472
+
473
+ pendingBytes += end - offset;
474
+ if (pendingBytes > MAX_FRAME_BYTES) throw liveTextProtocolFailure();
475
+ pending += decoder.decode(chunk.subarray(offset, end), { stream: newline < 0 });
476
+ offset = end + 1;
477
+ if (newline < 0) break;
478
+ const frame = decode(pending);
479
+
480
+ pending = "";
481
+ pendingBytes = 0;
482
+ if (accept(frame)) frames.push(frame);
483
+ }
484
+
485
+ return frames;
486
+ },
487
+ catch: liveTextProtocolFailure,
488
+ }),
489
+ ),
490
+ Stream.flatMap(Stream.fromArray),
491
+ // This connection is open-ended. Clean EOF is also loss of provisional state.
492
+ Stream.concat(Stream.fail(liveTextProtocolFailure())),
493
+ );
494
+ });
495
+
353
496
  const outOfContract = (threadId: string, operation: string, observed: string): ThreadClientError =>
354
497
  ThreadClientError.make({
355
498
  threadId,
@@ -361,6 +504,7 @@ const outOfContract = (threadId: string, operation: string, observed: string): T
361
504
  const hostRpcMethods = {
362
505
  submit: "submitEncoded",
363
506
  awaitSettlement: "awaitSettlementEncoded",
507
+ awaitSettlementRecord: "awaitSettlementRecordEncoded",
364
508
  submissionStatus: "submissionStatusEncoded",
365
509
  awaitProgress: "awaitProgressEncoded",
366
510
  cancelProgress: "cancelProgressEncoded",
@@ -385,6 +529,14 @@ export class CloudflareThreadClient extends Context.Service<
385
529
  receipt: Receipt,
386
530
  ) => Effect.Effect<SubmissionStatus, ClientAwaitFailure>;
387
531
  readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, ClientAwaitFailure>;
532
+ /**
533
+ * Wait for finalization and return this Receipt's canonical terminal record in one RPC.
534
+ * Requires both settlement and observation authority. Decode an ordinary completed
535
+ * record's result through the Agent output Schema; joined completion may have no result.
536
+ */
537
+ readonly awaitSettlementRecord: (
538
+ receipt: Receipt,
539
+ ) => Effect.Effect<SubmissionSettledRecord, ClientAwaitFailure>;
388
540
  /**
389
541
  * Wait without polling until progress after `afterSequence` is already durable or hinted.
390
542
  * The result is deliberately void: canonical records remain authoritative and must be read.
@@ -394,6 +546,14 @@ export class CloudflareThreadClient extends Context.Service<
394
546
  threadId: ThreadId,
395
547
  afterSequence: CanonicalSequence,
396
548
  ) => Effect.Effect<void, ClientProgressFailure>;
549
+ /**
550
+ * Current provisional text followed by native deltas. Reset starts a replacement snapshot,
551
+ * including when a slow consumer exceeds the fixed backlog. Reconcile by
552
+ * Run/Turn identity to canonical records; discard drafts on Discard, AttemptEnded,
553
+ * Reset or stream failure. Snapshots exist only in the Object incarnation's memory.
554
+ * Scope the stream: interruption cancels its remote subscription.
555
+ */
556
+ readonly watchText: (threadId: ThreadId) => Stream.Stream<LiveTextFrame, ClientObserveFailure>;
397
557
  /** One bounded page of canonical records. */
398
558
  readonly readPage: (
399
559
  threadId: ThreadId,
@@ -439,7 +599,10 @@ export class CloudflareThreadClient extends Context.Service<
439
599
  readonly rpcTracing?: string;
440
600
  }): Layer.Layer<CloudflareThreadClient> {
441
601
  return CloudflareThreadClient.layer.pipe(
442
- Layer.provide([ThreadObjectNamespace.layer(options.namespace, options), BrowserCrypto.layer]),
602
+ Layer.provide([
603
+ ThreadObjectNamespace.layer(options.namespace, options),
604
+ cloudflareCryptoLayer,
605
+ ]),
443
606
  );
444
607
  }
445
608
 
@@ -453,7 +616,7 @@ export class CloudflareThreadClient extends Context.Service<
453
616
  const { rpcTracing } = namespace;
454
617
  const crypto = yield* Crypto.Crypto;
455
618
 
456
- const call = Effect.fn(
619
+ const call = Effect.fnUntraced(
457
620
  function* (
458
621
  threadId: ThreadId,
459
622
  operation: keyof typeof hostRpcMethods,
@@ -676,9 +839,31 @@ export class CloudflareThreadClient extends Context.Service<
676
839
  return settled.settlement;
677
840
  }),
678
841
 
842
+ awaitSettlementRecord: (receipt) =>
843
+ Effect.gen(function* () {
844
+ const encoded = yield* encodeReceipt(receipt).pipe(
845
+ Effect.mapError((error) =>
846
+ HostProtocolError.make({
847
+ message: boundHostDiagnostic(`receipt encode failed: ${error.message}`),
848
+ }),
849
+ ),
850
+ );
851
+
852
+ const response = yield* call(receipt.threadId, "awaitSettlementRecord", encoded);
853
+
854
+ const settled = yield* expect(
855
+ receipt.threadId,
856
+ "awaitSettlementRecord",
857
+ SettlementRecordReached,
858
+ ClientAwaitHostFailure,
859
+ )(response);
860
+
861
+ return settled.record;
862
+ }),
863
+
679
864
  awaitProgress: (threadId, afterSequence) =>
680
865
  Effect.gen(function* () {
681
- const waiterId = yield* crypto.randomUUIDv4.pipe(
866
+ const waiterId = yield* crypto.randomUUIDv4().pipe(
682
867
  Effect.mapError((error) =>
683
868
  HostProtocolError.make({
684
869
  message: boundHostDiagnostic(
@@ -718,6 +903,101 @@ export class CloudflareThreadClient extends Context.Service<
718
903
  yield* attempt(0).pipe(Effect.onInterrupt(() => cancelProgress(threadId, waiterId)));
719
904
  }),
720
905
 
906
+ watchText: (threadId) =>
907
+ Stream.unwrap(
908
+ Effect.gen(function* () {
909
+ const traceArgs =
910
+ rpcTracing === undefined ? [] : yield* RpcTracing.withRpcTraceContext([]);
911
+
912
+ const openingFailure = (cause: unknown) =>
913
+ ThreadClientError.make({
914
+ threadId,
915
+ message: "Provisional text observation could not reach the Thread Object",
916
+ cause,
917
+ ...cloudflareFailureSignals(cause),
918
+ });
919
+
920
+ // Retain the original RPC promise: resolving through callThreadObject would
921
+ // erase its optional native disposer before an interrupted opening can use it.
922
+ const target = yield* RpcTargets.get(namespace, threadId, () =>
923
+ namespace.get(threadId),
924
+ ).pipe(Effect.mapError(openingFailure));
925
+
926
+ let request: Promise<unknown> | undefined;
927
+ let received: unknown;
928
+ let closed = false;
929
+
930
+ const cancel = () => {
931
+ const value = received;
932
+
933
+ received = undefined;
934
+
935
+ return value instanceof ReadableStream && !value.locked
936
+ ? value.cancel().catch(() => undefined)
937
+ : Promise.resolve();
938
+ };
939
+
940
+ // Install ownership before invoking foreign code, not after its promise settles.
941
+ yield* Effect.addFinalizer(() =>
942
+ Effect.try({
943
+ try: () => {
944
+ closed = true;
945
+ if (Predicate.hasProperty(request, Symbol.dispose)) {
946
+ const dispose = request[Symbol.dispose];
947
+
948
+ if (typeof dispose === "function") dispose.call(request);
949
+ }
950
+ },
951
+ catch: () => undefined,
952
+ }).pipe(
953
+ Effect.ignore,
954
+ Effect.andThen(
955
+ Effect.tryPromise({ try: cancel, catch: () => undefined }).pipe(
956
+ Effect.interruptible,
957
+ Effect.timeout(PROGRESS_CANCELLATION_TIMEOUT),
958
+ Effect.ignore,
959
+ ),
960
+ ),
961
+ ),
962
+ );
963
+
964
+ const raw = yield* Effect.tryPromise({
965
+ try: () => {
966
+ request = target.watchTextEncoded(
967
+ Schema.encodeSync(WatchTextRequest)({ schemaVersion: 1 }),
968
+ ...traceArgs,
969
+ );
970
+
971
+ return request.then((value) => {
972
+ received = value;
973
+
974
+ // A custom endpoint may return a plain, non-cancellable promise. Keep
975
+ // late cleanup on that native promise chain; it starts no Effect fiber.
976
+ return closed ? cancel().then(() => value) : value;
977
+ });
978
+ },
979
+ catch: openingFailure,
980
+ }).pipe(
981
+ Effect.interruptible,
982
+ Effect.tapCause(() => RpcTargets.invalidate(target)),
983
+ );
984
+
985
+ if (raw instanceof ReadableStream) return decodeLiveText(threadId, raw);
986
+
987
+ const response = yield* decodeHostResponse(raw).pipe(
988
+ Effect.mapError(liveTextProtocolFailure),
989
+ );
990
+
991
+ if (
992
+ response._tag === "HostFailed" &&
993
+ Schema.is(ClientObserveHostFailure)(response.failure)
994
+ )
995
+ return yield* response.failure;
996
+
997
+ return yield* liveTextProtocolFailure();
998
+ }),
999
+ ),
1000
+
721
1001
  readPage,
722
1002
 
723
1003
  readAll: (threadId) =>