@yielded/agent-platform-cloudflare 0.1.0-beta.173 → 0.1.0-beta.175

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 (70) hide show
  1. package/dist/{Alarm-BMEAZo3p.mjs → Alarm-B44efvB1.mjs} +162 -103
  2. package/dist/Alarm-B44efvB1.mjs.map +1 -0
  3. package/dist/Alarm.d.mts +2 -0
  4. package/dist/Alarm.mjs +1 -1
  5. package/dist/BrowserRestCapture.mjs.map +1 -1
  6. package/dist/BrowserRestCrawl.mjs.map +1 -1
  7. package/dist/{BrowserSession-Bx0V1-6_.d.mts → BrowserSession-Cojp7nth.d.mts} +3 -2
  8. package/dist/{BrowserSession-BfNmyayH.mjs → BrowserSession-aovMjfL4.mjs} +14 -14
  9. package/dist/BrowserSession-aovMjfL4.mjs.map +1 -0
  10. package/dist/BrowserSession.d.mts +1 -1
  11. package/dist/BrowserSession.mjs +2 -2
  12. package/dist/BrowserUse.d.mts +40 -64
  13. package/dist/BrowserUse.mjs +223 -2
  14. package/dist/BrowserUse.mjs.map +1 -0
  15. package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -1
  16. package/dist/CloudflareCodeMode.mjs.map +1 -1
  17. package/dist/CloudflareScheduling.mjs +9 -9
  18. package/dist/CloudflareScheduling.mjs.map +1 -1
  19. package/dist/CloudflareSubscriptions.mjs +15 -15
  20. package/dist/CloudflareSubscriptions.mjs.map +1 -1
  21. package/dist/{CloudflareThreadClient-DgZVrZzF.mjs → CloudflareThreadClient-Ai3MTs3X.mjs} +43 -40
  22. package/dist/CloudflareThreadClient-Ai3MTs3X.mjs.map +1 -0
  23. package/dist/CloudflareThreadClient.mjs +1 -1
  24. package/dist/{InteractiveBrowser-LdKzQeLR.mjs → InteractiveBrowser-BHNlwjZL.mjs} +40 -31
  25. package/dist/InteractiveBrowser-BHNlwjZL.mjs.map +1 -0
  26. package/dist/InteractiveBrowser.mjs +1 -1
  27. package/dist/{ThreadObject-CRbLbdXM.d.mts → ThreadObject-CxNlhoPL.d.mts} +3 -3
  28. package/dist/{ThreadObject-CGIaz6Jv.mjs → ThreadObject-DGvlHWYi.mjs} +50 -52
  29. package/dist/ThreadObject-DGvlHWYi.mjs.map +1 -0
  30. package/dist/ThreadObject.d.mts +1 -1
  31. package/dist/ThreadObject.mjs +1 -1
  32. package/dist/WakeScheduler.d.mts +1 -1
  33. package/dist/WakeScheduler.mjs +8 -3
  34. package/dist/WakeScheduler.mjs.map +1 -1
  35. package/dist/index.d.mts +2 -2
  36. package/dist/index.mjs +5 -5
  37. package/dist/{prepared-admission-CSdzFM_v.mjs → prepared-admission-DYdffQ8K.mjs} +2 -2
  38. package/dist/{prepared-admission-CSdzFM_v.mjs.map → prepared-admission-DYdffQ8K.mjs.map} +1 -1
  39. package/package.json +1 -1
  40. package/src/Alarm.ts +293 -155
  41. package/src/BrowserRestCapture.ts +1 -0
  42. package/src/BrowserRestCrawl.ts +1 -0
  43. package/src/BrowserSession.ts +3 -2
  44. package/src/BrowserUse.ts +287 -1504
  45. package/src/CloudflareCodeMode.ts +6 -0
  46. package/src/CloudflareScheduling.ts +103 -103
  47. package/src/CloudflareSubscriptions.ts +192 -199
  48. package/src/CloudflareThreadClient.ts +258 -269
  49. package/src/InteractiveBrowser.ts +43 -17
  50. package/src/ThreadObject.ts +51 -45
  51. package/src/WakeScheduler.ts +12 -9
  52. package/src/internal/browser-binding.ts +8 -0
  53. package/src/internal/browser-credentials.ts +18 -13
  54. package/src/internal/browser-file-selection.ts +13 -2
  55. package/src/internal/browser-quick-action.ts +1 -0
  56. package/src/internal/browser-session-lifecycle.ts +32 -36
  57. package/src/internal/due-queue.ts +38 -19
  58. package/src/internal/layers.ts +108 -87
  59. package/src/internal/message-delivery.ts +61 -37
  60. package/dist/Alarm-BMEAZo3p.mjs.map +0 -1
  61. package/dist/BrowserSession-BfNmyayH.mjs.map +0 -1
  62. package/dist/BrowserUse-Bc2JdLW2.mjs +0 -1528
  63. package/dist/BrowserUse-Bc2JdLW2.mjs.map +0 -1
  64. package/dist/CloudflareThreadClient-DgZVrZzF.mjs.map +0 -1
  65. package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +0 -1
  66. package/dist/ThreadObject-CGIaz6Jv.mjs.map +0 -1
  67. package/dist/page-function-CmUCcyNP.mjs +0 -11
  68. package/dist/page-function-CmUCcyNP.mjs.map +0 -1
  69. package/src/internal/browser-dom.ts +0 -886
  70. package/src/internal/browser-jev-dom.ts +0 -362
@@ -49,7 +49,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
49
49
  !Schema.is(Account)(options.accountId) ||
50
50
  Redacted.value(options.apiToken).length === 0
51
51
  ) {
52
- return yield* new BrowserRunCleanupError({ reason: "configuration" });
52
+ return yield* BrowserRunCleanupError.make({ reason: "configuration" });
53
53
  }
54
54
  const client = yield* HttpClient.HttpClient;
55
55
 
@@ -65,39 +65,38 @@ export class BrowserRunSessionLifecycle extends Context.Service<
65
65
  .pipe(
66
66
  // workerd supports manual/follow, not error. Reject every redirect below.
67
67
  Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }),
68
- Effect.mapError(
69
- (cause) =>
70
- new BrowserRunCleanupError({
71
- reason: "provider",
72
- cause: browserFailure("browser.close.request", cause),
73
- }),
68
+ Effect.mapError((cause) =>
69
+ BrowserRunCleanupError.make({
70
+ reason: "provider",
71
+ cause: browserFailure("browser.close.request", cause),
72
+ }),
74
73
  ),
75
74
  );
76
75
 
77
76
  if (response.status === 401 || response.status === 403)
78
- return yield* new BrowserRunCleanupError({
77
+ return yield* BrowserRunCleanupError.make({
79
78
  reason: "authorization",
80
79
  status: response.status,
81
80
  });
82
81
  if (response.status === 429)
83
- return yield* new BrowserRunCleanupError({
82
+ return yield* BrowserRunCleanupError.make({
84
83
  reason: "rate-limited",
85
84
  status: response.status,
86
85
  });
87
86
  if (response.status !== 200 && response.status !== 404)
88
- return yield* new BrowserRunCleanupError({
87
+ return yield* BrowserRunCleanupError.make({
89
88
  reason: "provider",
90
89
  status: response.status,
91
90
  });
92
91
  if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json")
93
- return yield* new BrowserRunCleanupError({ reason: "malformed" });
92
+ return yield* BrowserRunCleanupError.make({ reason: "malformed" });
94
93
 
95
94
  const bytes = yield* Stream.runFoldEffect(
96
95
  response.stream,
97
96
  () => new Uint8Array(),
98
97
  (body, chunk) => {
99
98
  if (body.byteLength + chunk.byteLength > 16_384)
100
- return Effect.fail(new BrowserRunCleanupError({ reason: "malformed" }));
99
+ return Effect.fail(BrowserRunCleanupError.make({ reason: "malformed" }));
101
100
  const combined = new Uint8Array(body.byteLength + chunk.byteLength);
102
101
 
103
102
  combined.set(body);
@@ -106,19 +105,18 @@ export class BrowserRunSessionLifecycle extends Context.Service<
106
105
  return Effect.succeed(combined);
107
106
  },
108
107
  ).pipe(
109
- Effect.mapError(
110
- (cause) =>
111
- new BrowserRunCleanupError({
112
- reason: "malformed",
113
- cause: browserFailure("browser.close.response", cause),
114
- }),
108
+ Effect.mapError((cause) =>
109
+ BrowserRunCleanupError.make({
110
+ reason: "malformed",
111
+ cause: browserFailure("browser.close.response", cause),
112
+ }),
115
113
  ),
116
114
  );
117
115
 
118
116
  const body = yield* Effect.try({
119
117
  try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes),
120
118
  catch: (cause) =>
121
- new BrowserRunCleanupError({
119
+ BrowserRunCleanupError.make({
122
120
  reason: "malformed",
123
121
  cause: browserFailure("browser.close.decode", cause),
124
122
  }),
@@ -131,16 +129,15 @@ export class BrowserRunSessionLifecycle extends Context.Service<
131
129
 
132
130
  if (Option.isSome(absent)) return true;
133
131
 
134
- return yield* new BrowserRunCleanupError({ reason: "malformed" });
132
+ return yield* BrowserRunCleanupError.make({ reason: "malformed" });
135
133
  }
136
134
  if (method === "DELETE") {
137
135
  const result = yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(
138
- Effect.mapError(
139
- (cause) =>
140
- new BrowserRunCleanupError({
141
- reason: "malformed",
142
- cause: browserFailure("browser.close.response", cause),
143
- }),
136
+ Effect.mapError((cause) =>
137
+ BrowserRunCleanupError.make({
138
+ reason: "malformed",
139
+ cause: browserFailure("browser.close.response", cause),
140
+ }),
144
141
  ),
145
142
  );
146
143
 
@@ -148,17 +145,16 @@ export class BrowserRunSessionLifecycle extends Context.Service<
148
145
  }
149
146
 
150
147
  const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(
151
- Effect.mapError(
152
- (cause) =>
153
- new BrowserRunCleanupError({
154
- reason: "malformed",
155
- cause: browserFailure("browser.close.response", cause),
156
- }),
148
+ Effect.mapError((cause) =>
149
+ BrowserRunCleanupError.make({
150
+ reason: "malformed",
151
+ cause: browserFailure("browser.close.response", cause),
152
+ }),
157
153
  ),
158
154
  );
159
155
 
160
156
  if (result.sessionId !== sessionId)
161
- return yield* new BrowserRunCleanupError({ reason: "malformed" });
157
+ return yield* BrowserRunCleanupError.make({ reason: "malformed" });
162
158
 
163
159
  return result.endTime !== undefined && result.endTime > 0;
164
160
  });
@@ -166,7 +162,7 @@ export class BrowserRunSessionLifecycle extends Context.Service<
166
162
  const close = Effect.fnUntraced(
167
163
  function* (sessionId: Redacted.Redacted<string>) {
168
164
  const id = yield* Schema.decodeEffect(Identity)(Redacted.value(sessionId)).pipe(
169
- Effect.mapError(() => new BrowserRunCleanupError({ reason: "configuration" })),
165
+ Effect.mapError(() => BrowserRunCleanupError.make({ reason: "configuration" })),
170
166
  );
171
167
 
172
168
  if (yield* request("DELETE", id)) return;
@@ -174,11 +170,11 @@ export class BrowserRunSessionLifecycle extends Context.Service<
174
170
  if (yield* request("GET", id)) return;
175
171
  }
176
172
 
177
- return yield* new BrowserRunCleanupError({ reason: "pending" });
173
+ return yield* BrowserRunCleanupError.make({ reason: "pending" });
178
174
  },
179
175
  Effect.timeoutOrElse({
180
176
  duration: "10 seconds",
181
- orElse: () => Effect.fail(new BrowserRunCleanupError({ reason: "timeout" })),
177
+ orElse: () => Effect.fail(BrowserRunCleanupError.make({ reason: "timeout" })),
182
178
  }),
183
179
  Effect.withTracerEnabled(false),
184
180
  );
@@ -139,10 +139,9 @@ export const make = (storage: DurableObjectStorage) => {
139
139
 
140
140
  // Nine bindings per lane stay below Durable Object SQLite's 100-parameter limit.
141
141
  const write = (changes: ReadonlyArray<Change>) => {
142
- const rows = decode(
143
- sql
144
- .exec(
145
- `WITH changes (id, revision, dueAt, stalls, progressKey, notBefore, state, reported, expected_revision) AS (
142
+ const rows = sql
143
+ .exec(
144
+ `WITH changes (id, revision, dueAt, stalls, progressKey, notBefore, state, reported, expected_revision) AS (
146
145
  VALUES ${changes.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ")}
147
146
  ) INSERT INTO platform_cloudflare_due_queue
148
147
  (id, revision, dueAt, stalls, progressKey, notBefore, state, reported)
@@ -152,24 +151,24 @@ export const make = (storage: DurableObjectStorage) => {
152
151
  notBefore = excluded.notBefore, state = excluded.state, reported = excluded.reported
153
152
  WHERE platform_cloudflare_due_queue.revision = (
154
153
  SELECT expected_revision FROM changes WHERE changes.id = excluded.id
155
- ) RETURNING id, revision, dueAt, stalls, progressKey, notBefore, state, reported`,
156
- ...changes.flatMap(({ before, after }) => [
157
- after.id,
158
- after.revision,
159
- after.dueAt,
160
- after.stalls,
161
- after.progressKey,
162
- after.notBefore,
163
- after.state,
164
- after.reported,
165
- before?.revision ?? -1,
166
- ]),
167
- )
168
- .toArray(),
169
- );
154
+ ) RETURNING 1`,
155
+ ...changes.flatMap(({ before, after }) => [
156
+ after.id,
157
+ after.revision,
158
+ after.dueAt,
159
+ after.stalls,
160
+ after.progressKey,
161
+ after.notBefore,
162
+ after.state,
163
+ after.reported,
164
+ before?.revision ?? -1,
165
+ ]),
166
+ )
167
+ .toArray();
170
168
 
171
169
  // Partial revision acceptance must reject the enclosing source transaction.
172
170
  if (rows.length !== changes.length)
171
+ // oxlint-disable-next-line effect/no-untyped-throw, effect/no-native-error -- Native storage transaction must roll back partial revision acceptance; its Effect adapter maps the cause.
173
172
  throw new Error("Maintenance due revision changed during transaction");
174
173
  };
175
174
 
@@ -204,6 +203,7 @@ export const make = (storage: DurableObjectStorage) => {
204
203
  return result;
205
204
  } catch (cause) {
206
205
  if (frame !== undefined) finish(frame, false);
206
+ // oxlint-disable-next-line effect/no-untyped-throw -- Native transaction Promise must reject with the original failure after invalidating speculative state.
207
207
  throw cause;
208
208
  }
209
209
  };
@@ -302,6 +302,24 @@ export const make = (storage: DurableObjectStorage) => {
302
302
  );
303
303
  };
304
304
 
305
+ /** Native checkpoints fence the generation, not each source commit. Once progress
306
+ * resets a claimed lane, further native commits owe the same recovery opportunity.
307
+ * A new claim, deadline, or retry budget makes the next commit advance it again. */
308
+ const progressNative = (dueAt: number) => {
309
+ const row = read().find((row) => row.id === Native);
310
+
311
+ if (
312
+ row?.state === "pending" &&
313
+ row.dueAt !== null &&
314
+ row.dueAt <= dueAt &&
315
+ row.stalls === 0 &&
316
+ row.notBefore === 0 &&
317
+ row.reported === 0
318
+ )
319
+ return;
320
+ dirty(Native, dueAt, true);
321
+ };
322
+
305
323
  /** A strictly increasing source cursor; old or repeated notices cannot renew a budget. */
306
324
  const progress = (id: string, dueAt: number, cursor: bigint) => {
307
325
  Schema.decodeSync(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(cursor);
@@ -416,6 +434,7 @@ export const make = (storage: DurableObjectStorage) => {
416
434
  transaction,
417
435
  read,
418
436
  dirty,
437
+ progressNative,
419
438
  progress,
420
439
  register,
421
440
  claim,
@@ -134,7 +134,7 @@ import * as DueQueue from "./due-queue.ts";
134
134
  import { LiveTextHub } from "./live-text.ts";
135
135
  import {
136
136
  guardedMessageDeliveryStoreLayer,
137
- threadMessageDeliveryLayer,
137
+ lazyThreadMessageDeliveryLayer,
138
138
  } from "./message-delivery.ts";
139
139
  import { cloudflarePreparedInputAdmissionLayer } from "./prepared-admission.ts";
140
140
  import { ProgressWaitRegistry } from "./progress-wait.ts";
@@ -625,10 +625,14 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
625
625
  verifyOnOpen: config.verifyOnOpen,
626
626
  };
627
627
 
628
- const infrastructure = Layer.mergeAll(
629
- options.lifecyclePublication === undefined ? Layer.empty : lifecyclePublicationLayer,
630
- storageConfigLayer(storageOptions),
631
- Layer.effect(SqlClient)(SqlClient),
628
+ // Memoize these shared compositions within this Object's build, so repeated
629
+ // consumers reuse their contexts as well as the scoped leaf services.
630
+ const infrastructure = Layer.suspend(() =>
631
+ Layer.mergeAll(
632
+ options.lifecyclePublication === undefined ? Layer.empty : lifecyclePublicationLayer,
633
+ storageConfigLayer(storageOptions),
634
+ Layer.effect(SqlClient)(SqlClient),
635
+ ),
632
636
  );
633
637
 
634
638
  // Preserve the database owner's reader gate and rollback invalidators, while
@@ -638,6 +642,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
638
642
  const owner = yield* SqlStorageOwner;
639
643
  const mutations = yield* ThreadMutationGate;
640
644
 
645
+ // oxlint-disable-next-line effect/no-native-error -- Missing host SQL ownership is an assembly defect, not a recoverable workflow failure.
641
646
  if (owner === undefined) return yield* Effect.die(new Error("SQL owner unavailable"));
642
647
 
643
648
  return {
@@ -660,7 +665,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
660
665
  }),
661
666
  ).pipe(Layer.provide(sqlOwnerLayer));
662
667
 
663
- const localInfrastructure = Layer.mergeAll(infrastructure, sourceOwner);
668
+ const localInfrastructure = Layer.suspend(() => Layer.mergeAll(infrastructure, sourceOwner));
664
669
 
665
670
  const sourceProgress = Layer.effect(SqlStorageProgress)(
666
671
  Effect.gen(function* () {
@@ -708,41 +713,57 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
708
713
 
709
714
  // The same local ports serve routed decorators and owner-side RPC execution.
710
715
  // The RPC executor must never receive routed ports and bounce requests between Objects.
711
- const rawLocalPorts = Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(
712
- Layer.provide(localInfrastructure),
713
- Layer.provide(sourceProgress),
716
+ const rawLocalPorts = Layer.suspend(() =>
717
+ Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(
718
+ Layer.provide(localInfrastructure),
719
+ Layer.provide(sourceProgress),
720
+ ),
721
+ );
722
+
723
+ const base = Layer.suspend(() =>
724
+ Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer),
714
725
  );
715
726
 
716
- const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
717
727
  const wakes = cloudflareWakeSchedulerLayer.pipe(Layer.provide(base));
718
728
 
719
- const messageStore = guardedMessageDeliveryStoreLayer.pipe(
720
- Layer.provide(
721
- doMessageDeliveryStoreLayer().pipe(
722
- Layer.provide(localInfrastructure),
723
- Layer.provide(sourceProgress),
729
+ const messageStore = Layer.suspend(() =>
730
+ guardedMessageDeliveryStoreLayer.pipe(
731
+ Layer.provide(
732
+ doMessageDeliveryStoreLayer().pipe(
733
+ Layer.provide(localInfrastructure),
734
+ Layer.provide(sourceProgress),
735
+ ),
724
736
  ),
737
+ Layer.provide(wakes),
725
738
  ),
726
- Layer.provide(wakes),
727
739
  );
728
740
 
729
- const messageRecovery = threadMessageDeliveryLayer.pipe(
730
- // Four parallel attempts per wave. Native completion stops new wake-driven waves;
731
- // retained retry deadlines schedule future alarms.
732
- Layer.provide(MessageDeliveryDriver.layer({ batchSize: 4, concurrency: 4 })),
733
- Layer.provide(cloudflarePreparedInputAdmissionLayer),
734
- Layer.provide(CloudflareThreadClient.layer),
735
- Layer.provide(messageStore),
736
- Layer.provide(wakes),
741
+ const messageRecovery = Layer.suspend(() =>
742
+ lazyThreadMessageDeliveryLayer(
743
+ // Four parallel attempts per wave. Native completion stops new wake-driven waves;
744
+ // retained retry deadlines schedule future alarms.
745
+ MessageDeliveryDriver.layer({ batchSize: 4, concurrency: 4 }).pipe(
746
+ Layer.provide(cloudflarePreparedInputAdmissionLayer),
747
+ Layer.provide(CloudflareThreadClient.layer),
748
+ Layer.provide(messageStore),
749
+ Layer.provide(wakes),
750
+ ),
751
+ ).pipe(Layer.provide(messageStore)),
737
752
  );
738
753
 
739
- const publication = (options.publication ?? ThreadPublication.layer).pipe(
740
- Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
741
- );
754
+ const publication =
755
+ options.publication === undefined
756
+ ? ThreadPublication.layer
757
+ : options.publication.pipe(
758
+ Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
759
+ );
742
760
 
743
- const projection = (options.projection ?? ThreadProjectionMaintenance.layer).pipe(
744
- Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
745
- );
761
+ const projection =
762
+ options.projection === undefined
763
+ ? ThreadProjectionMaintenance.layer
764
+ : options.projection.pipe(
765
+ Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
766
+ );
746
767
 
747
768
  const localPorts =
748
769
  options.publication === undefined &&
@@ -889,71 +910,71 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
889
910
  return Context.make(ThreadStore, observedStore).pipe(
890
911
  Context.add(StorageOwner, storageOwner),
891
912
  Context.add(SettlementPublisher, {
892
- publish: (input) =>
893
- Effect.gen(function* () {
894
- const { request } = yield* validatePublication(input);
895
-
896
- return yield* mutations
897
- .withMutation(
898
- sourceCommits
899
- .withPermit(
900
- settlementPublisher.publish(request).pipe(
901
- Effect.tap((result) =>
902
- result.replayed
903
- ? Effect.void
904
- : index
905
- .applyCommitted(
906
- request.append,
907
- AppendResult.make({
908
- firstSequence: result.tailSequence,
909
- lastSequence: result.tailSequence,
910
-
911
- replayed: false,
912
- }),
913
- )
914
- .pipe(
915
- Effect.catchCause((cause) =>
916
- Cause.hasInterrupts(cause)
917
- ? Effect.interrupt
918
- : Effect.logError(
919
- "Thread projection deferred after settlement publication",
920
- cause,
921
- ),
922
- ),
913
+ publish: Effect.fnUntraced(function* (input) {
914
+ const { request } = yield* validatePublication(input);
915
+
916
+ return yield* mutations
917
+ .withMutation(
918
+ sourceCommits
919
+ .withPermit(
920
+ settlementPublisher.publish(request).pipe(
921
+ Effect.tap((result) =>
922
+ result.replayed
923
+ ? Effect.void
924
+ : index
925
+ .applyCommitted(
926
+ request.append,
927
+ AppendResult.make({
928
+ firstSequence: result.tailSequence,
929
+ lastSequence: result.tailSequence,
930
+
931
+ replayed: false,
932
+ }),
933
+ )
934
+ .pipe(
935
+ Effect.catchCause((cause) =>
936
+ Cause.hasInterrupts(cause)
937
+ ? Effect.interrupt
938
+ : Effect.logError(
939
+ "Thread projection deferred after settlement publication",
940
+ cause,
941
+ ),
923
942
  ),
924
- ),
943
+ ),
925
944
  ),
926
- )
927
- .pipe(Effect.tap(() => afterCommit)),
928
- {
929
- lanes: [
930
- ...(options.projection === undefined ? [] : [DueQueue.Projection]),
931
- ...(options.lifecyclePublication === undefined
932
- ? []
933
- : [DueQueue.Lifecycle]),
934
- ],
935
- },
936
- )
937
- .pipe(
938
- Effect.catchTag("DurableAlarmError", (cause) =>
939
- ThreadStoreError.make({
940
- operation: "prearm settlement publication",
941
- message: cause.message,
942
- cause,
943
- }),
944
- ),
945
- );
946
- }),
945
+ ),
946
+ )
947
+ .pipe(Effect.tap(() => afterCommit)),
948
+ {
949
+ lanes: [
950
+ ...(options.projection === undefined ? [] : [DueQueue.Projection]),
951
+ ...(options.lifecyclePublication === undefined
952
+ ? []
953
+ : [DueQueue.Lifecycle]),
954
+ ],
955
+ },
956
+ )
957
+ .pipe(
958
+ Effect.catchTag("DurableAlarmError", (cause) =>
959
+ ThreadStoreError.make({
960
+ operation: "prearm settlement publication",
961
+ message: cause.message,
962
+ cause,
963
+ }),
964
+ ),
965
+ );
966
+ }),
947
967
  }),
948
968
  Context.add(SubmissionLedger, {
949
969
  ...ledger,
950
- ...(options.hostLanesForMutation === undefined
970
+ ...(options.hostLanesForMutation === undefined &&
971
+ options.lifecyclePublication === undefined
951
972
  ? {}
952
973
  : {
953
- admit: (request: AdmissionRequest) =>
974
+ admit: (...args: Parameters<SubmissionLedger["Service"]["admit"]>) =>
954
975
  observeMutation(
955
- { _tag: "Admission", request },
956
- ledger.admit(request),
976
+ { _tag: "Admission", request: args[0] },
977
+ ledger.admit(...args),
957
978
  false,
958
979
  ),
959
980
  }),
@@ -5,7 +5,7 @@ import {
5
5
  MessageDeliveryStore,
6
6
  } from "@yielded/agent/message-delivery";
7
7
  import { WakeScheduler } from "@yielded/agent/wake-scheduler";
8
- import { Clock, Context, Effect, Layer, Option } from "effect";
8
+ import { Clock, Context, Duration, Effect, Exit, Layer, Option } from "effect";
9
9
 
10
10
  import { DurableAlarmError, ThreadMessageDelivery, ThreadMutationGate } from "../Alarm.ts";
11
11
  import { ThreadObjectPlacement } from "../CloudflareBindings.ts";
@@ -76,45 +76,69 @@ export const guardedMessageDeliveryStoreLayer = Layer.effect(
76
76
  }),
77
77
  );
78
78
 
79
- /** Message progress shares the native alarm slot without blocking source runtime work. */
80
- export const threadMessageDeliveryLayer = Layer.effectContext(
81
- Effect.gen(function* () {
82
- const driver = yield* MessageDeliveryDriver;
83
- const store = yield* MessageDeliveryStore;
79
+ /** Build the driver in the Object's Scope only when retained delivery work is due. */
80
+ export const lazyThreadMessageDeliveryLayer = <E, R>(
81
+ driver: Layer.Layer<MessageDeliveryDriver, E, R>,
82
+ ) =>
83
+ Layer.fromBuildMemo((memoMap, scope) =>
84
+ Effect.gen(function* () {
85
+ const context = yield* Effect.context<R>();
86
+ const store = yield* MessageDeliveryStore;
84
87
 
85
- const failure = (operation: string) => () =>
86
- DurableAlarmError.make({
87
- operation,
88
- message: "Durable message recovery remains pending",
89
- });
88
+ // Failed builds release their Layer scopes; let a later wave build again.
89
+ const service = yield* Effect.cachedWithTTL(
90
+ Layer.buildWithMemoMap(driver, memoMap, scope).pipe(
91
+ Effect.provide(context),
92
+ Effect.map(Context.get(MessageDeliveryDriver)),
93
+ Effect.mapError((cause) =>
94
+ DurableAlarmError.make({
95
+ operation: "prepare message delivery",
96
+ message: "Durable message recovery remains pending",
97
+ cause,
98
+ }),
99
+ ),
100
+ ),
101
+ (exit) => (Exit.isSuccess(exit) ? Duration.infinity : Duration.zero),
102
+ );
90
103
 
91
- const prepare = Effect.gen(function* () {
92
- const deadline = yield* store.nextDeadline();
104
+ const failure = (operation: string) => () =>
105
+ DurableAlarmError.make({
106
+ operation,
107
+ message: "Durable message recovery remains pending",
108
+ });
93
109
 
94
- if (deadline === null || deadline > (yield* Clock.currentTimeMillis))
95
- return { timeoutMillis: 1, run: Effect.succeed(Option.fromNullishOr(deadline)) };
96
- // The assembled driver has four permits: a wave is one parallel attempt window.
97
- const keys = yield* store.due(yield* Clock.currentTimeMillis, 4);
98
- const records = yield* Effect.forEach(keys, (key) => store.get(key));
110
+ const prepare = Effect.gen(function* () {
111
+ const deadline = yield* store.nextDeadline();
99
112
 
100
- return {
101
- timeoutMillis: Math.max(
102
- 1,
103
- ...records.map((record) => record?.policy.attemptTimeoutMillis ?? 1),
104
- ),
105
- run: Effect.forEach(keys, (key) => driver.process(key), {
106
- concurrency: 4,
107
- discard: true,
108
- }).pipe(
109
- Effect.andThen(store.nextDeadline()),
110
- Effect.map(Option.fromNullishOr),
111
- Effect.mapError(failure("dispatch message delivery")),
112
- ),
113
- };
114
- }).pipe(Effect.mapError(failure("prepare message delivery")));
113
+ if (deadline === null || deadline > (yield* Clock.currentTimeMillis))
114
+ return { timeoutMillis: 1, run: Effect.succeed(Option.fromNullishOr(deadline)) };
115
+ // The assembled driver has four permits: a wave is one parallel attempt window.
116
+ const keys = yield* store.due(yield* Clock.currentTimeMillis, 4);
117
+ const records = yield* Effect.forEach(keys, (key) => store.get(key));
115
118
 
116
- return Context.make(ThreadMessageDelivery, {
117
- prepare,
118
- });
119
- }),
119
+ return {
120
+ timeoutMillis: Math.max(
121
+ 1,
122
+ ...records.map((record) => record?.policy.attemptTimeoutMillis ?? 1),
123
+ ),
124
+ run: Effect.flatMap(service, (driver) =>
125
+ Effect.forEach(keys, (key) => driver.process(key), {
126
+ concurrency: 4,
127
+ discard: true,
128
+ }),
129
+ ).pipe(
130
+ Effect.andThen(store.nextDeadline()),
131
+ Effect.map(Option.fromNullishOr),
132
+ Effect.mapError(failure("dispatch message delivery")),
133
+ ),
134
+ };
135
+ }).pipe(Effect.mapError(failure("prepare message delivery")));
136
+
137
+ return Context.make(ThreadMessageDelivery, { prepare });
138
+ }),
139
+ );
140
+
141
+ /** Message progress shares the native alarm slot without blocking source runtime work. */
142
+ export const threadMessageDeliveryLayer = lazyThreadMessageDeliveryLayer(
143
+ Layer.effect(MessageDeliveryDriver, MessageDeliveryDriver),
120
144
  );