@yielded/agent-platform-cloudflare 0.0.0-stage → 0.1.0-beta.166

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Alarm-B0ODr1NE.mjs +1738 -0
  3. package/dist/Alarm-B0ODr1NE.mjs.map +1 -0
  4. package/dist/Alarm.d.mts +267 -0
  5. package/dist/Alarm.mjs +2 -0
  6. package/dist/BrowserCredentials.d.mts +91 -0
  7. package/dist/BrowserCredentials.mjs +120 -0
  8. package/dist/BrowserCredentials.mjs.map +1 -0
  9. package/dist/BrowserRestCapture.d.mts +34 -0
  10. package/dist/BrowserRestCapture.mjs +238 -0
  11. package/dist/BrowserRestCapture.mjs.map +1 -0
  12. package/dist/BrowserRestCrawl.d.mts +16 -0
  13. package/dist/BrowserRestCrawl.mjs +244 -0
  14. package/dist/BrowserRestCrawl.mjs.map +1 -0
  15. package/dist/BrowserSession-B969Xg-2.d.mts +132 -0
  16. package/dist/BrowserSession-RfIzxGCH.mjs +515 -0
  17. package/dist/BrowserSession-RfIzxGCH.mjs.map +1 -0
  18. package/dist/BrowserSession.d.mts +3 -0
  19. package/dist/BrowserSession.mjs +4 -0
  20. package/dist/CloudflareAiGateway.d.mts +64 -0
  21. package/dist/CloudflareAiGateway.mjs +70 -0
  22. package/dist/CloudflareAiGateway.mjs.map +1 -0
  23. package/dist/CloudflareBindings.d.mts +115 -0
  24. package/dist/CloudflareBindings.mjs +113 -0
  25. package/dist/CloudflareBindings.mjs.map +1 -0
  26. package/dist/CloudflareBrowser-5an6m-Qb.mjs +495 -0
  27. package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +1 -0
  28. package/dist/CloudflareBrowser-gu7gma0Y.d.mts +80 -0
  29. package/dist/CloudflareBrowser.d.mts +2 -0
  30. package/dist/CloudflareBrowser.mjs +2 -0
  31. package/dist/CloudflareCodeMode.d.mts +44 -0
  32. package/dist/CloudflareCodeMode.mjs +616 -0
  33. package/dist/CloudflareCodeMode.mjs.map +1 -0
  34. package/dist/CloudflareConfig-qDrRLofL.d.mts +100 -0
  35. package/dist/CloudflareConfig.d.mts +2 -0
  36. package/dist/CloudflareConfig.mjs +122 -0
  37. package/dist/CloudflareConfig.mjs.map +1 -0
  38. package/dist/CloudflareMemory.d.mts +142 -0
  39. package/dist/CloudflareMemory.mjs +202 -0
  40. package/dist/CloudflareMemory.mjs.map +1 -0
  41. package/dist/CloudflareScheduling.d.mts +52 -0
  42. package/dist/CloudflareScheduling.mjs +396 -0
  43. package/dist/CloudflareScheduling.mjs.map +1 -0
  44. package/dist/CloudflareSubscriptions.d.mts +87 -0
  45. package/dist/CloudflareSubscriptions.mjs +529 -0
  46. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  47. package/dist/CloudflareThreadClient.d.mts +1796 -0
  48. package/dist/CloudflareThreadClient.mjs +382 -0
  49. package/dist/CloudflareThreadClient.mjs.map +1 -0
  50. package/dist/InteractiveBrowser-BbG3kGA9.mjs +1934 -0
  51. package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +1 -0
  52. package/dist/InteractiveBrowser-wV2Bti5e.d.mts +228 -0
  53. package/dist/InteractiveBrowser.d.mts +2 -0
  54. package/dist/InteractiveBrowser.mjs +2 -0
  55. package/dist/ThreadObject-BcfXbzSO.d.mts +867 -0
  56. package/dist/ThreadObject-DHjrnlX4.mjs +937 -0
  57. package/dist/ThreadObject-DHjrnlX4.mjs.map +1 -0
  58. package/dist/ThreadObject.d.mts +2 -0
  59. package/dist/ThreadObject.mjs +3 -0
  60. package/dist/WakeScheduler.d.mts +23 -0
  61. package/dist/WakeScheduler.mjs +57 -0
  62. package/dist/WakeScheduler.mjs.map +1 -0
  63. package/dist/boundary-BguazVkh.mjs +42 -0
  64. package/dist/boundary-BguazVkh.mjs.map +1 -0
  65. package/dist/index.d.mts +15 -0
  66. package/dist/index.mjs +15 -0
  67. package/dist/prepared-admission-DMX5p9dC.mjs +73 -0
  68. package/dist/prepared-admission-DMX5p9dC.mjs.map +1 -0
  69. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  70. package/package.json +1 -6
  71. package/src/Alarm.ts +2961 -0
  72. package/src/BrowserCredentials.ts +146 -0
  73. package/src/BrowserRestCapture.ts +477 -0
  74. package/src/BrowserRestCrawl.ts +540 -0
  75. package/src/BrowserSession.ts +667 -0
  76. package/src/CloudflareAiGateway.ts +170 -0
  77. package/src/CloudflareBindings.ts +199 -0
  78. package/src/CloudflareBrowser.ts +15 -0
  79. package/src/CloudflareCodeMode.ts +878 -0
  80. package/src/CloudflareConfig.ts +138 -0
  81. package/src/CloudflareMemory.ts +420 -0
  82. package/src/CloudflareScheduling.ts +745 -0
  83. package/src/CloudflareSubscriptions.ts +1044 -0
  84. package/src/CloudflareThreadClient.ts +809 -0
  85. package/src/InteractiveBrowser.ts +3275 -0
  86. package/src/ThreadObject.ts +1198 -0
  87. package/src/WakeScheduler.ts +91 -0
  88. package/src/index.ts +14 -0
  89. package/src/internal/boundary.ts +55 -0
  90. package/src/internal/browser-binding.ts +322 -0
  91. package/src/internal/browser-credentials.ts +362 -0
  92. package/src/internal/browser-failure.ts +92 -0
  93. package/src/internal/browser-file-selection.ts +126 -0
  94. package/src/internal/browser-quick-action.ts +857 -0
  95. package/src/internal/browser-readonly-live-view.ts +111 -0
  96. package/src/internal/browser-session-lifecycle.ts +193 -0
  97. package/src/internal/due-queue.ts +493 -0
  98. package/src/internal/layers.ts +1068 -0
  99. package/src/internal/message-delivery.ts +120 -0
  100. package/src/internal/prepared-admission.ts +116 -0
  101. package/src/internal/progress-wait.ts +121 -0
  102. package/src/internal/transport.ts +56 -0
  103. package/README.md +0 -3
@@ -0,0 +1,1068 @@
1
+ import { BrowserCrypto } from "@effect/platform-browser";
2
+ import { SqliteClient } from "@effect/sql-sqlite-do";
3
+ import { doMessageDeliveryStoreLayer } from "@yielded/agent-storage-cloudflare/do-message-delivery-store";
4
+ import {
5
+ type DoStorageFailpointHandler,
6
+ type DoStorageFailpoint,
7
+ } from "@yielded/agent-storage-cloudflare/do-storage-failpoint";
8
+ import { submissionLedgerLayer } from "@yielded/agent-storage-cloudflare/do-submission-ledger";
9
+ import {
10
+ threadStoreLayer,
11
+ storageConfigLayer,
12
+ storageFailpointLayer,
13
+ sqlOwnerLayer,
14
+ type DoStorageInitializationError,
15
+ type DoStorageOptions,
16
+ } from "@yielded/agent-storage-cloudflare/do-thread-store";
17
+ import {
18
+ type PortRequest,
19
+ type PortResponse,
20
+ } from "@yielded/agent-storage-cloudflare/port-protocol";
21
+ import {
22
+ executePortRequest,
23
+ makeLocalSubmissionLookup,
24
+ routedMessageDeliveryStoreLayer,
25
+ routedThreadStoreLayer,
26
+ routedSubmissionLedgerLayer,
27
+ routedWorkerAdmissionLayer,
28
+ } from "@yielded/agent-storage-cloudflare/port-routing";
29
+ import {
30
+ SqlStorageProgress,
31
+ SqlStorageProgressError,
32
+ } from "@yielded/agent-storage-sql/sql-storage-progress";
33
+ import {
34
+ compileRegistrations,
35
+ type AgentRegistration,
36
+ type ResolvedBinding,
37
+ } from "@yielded/agent/agent-registration";
38
+ import { type DigestError } from "@yielded/agent/digest";
39
+ import { DurableAgentRuntime, DurableRuntimeConfig } from "@yielded/agent/durable-agent-runtime";
40
+ import {
41
+ DurableRuntimeFailpoint,
42
+ type DurableRuntimeFailpointHandler,
43
+ } from "@yielded/agent/durable-failpoint";
44
+ import { ThreadId, type SubmissionId } from "@yielded/agent/identifiers";
45
+ import type { LifecyclePublicationHandler } from "@yielded/agent/lifecycle-publication";
46
+ import {
47
+ drainLifecyclePublications,
48
+ LifecyclePublicationError,
49
+ LifecyclePublicationFact,
50
+ lifecyclePublicationLayer,
51
+ } from "@yielded/agent/lifecycle-publication";
52
+ import {
53
+ type MessageDeliveryStore,
54
+ MessageDeliveryDriver,
55
+ type MessageDeliveryError,
56
+ } from "@yielded/agent/message-delivery";
57
+ import {
58
+ operationAuthorizerLayer,
59
+ type OperationAuthorizerService,
60
+ } from "@yielded/agent/operation-authorizer";
61
+ import { ProducerId } from "@yielded/agent/records";
62
+ import {
63
+ type RunContextPreparation,
64
+ type RunCostEstimator,
65
+ type RunToolFailureObserver,
66
+ } from "@yielded/agent/run-options";
67
+ import {
68
+ CurrentToolFailureObserver,
69
+ RunContextPreparationPassthrough,
70
+ RunToolAuthorization,
71
+ toolFailureObserverLayer,
72
+ } from "@yielded/agent/run-options";
73
+ import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
74
+ import {
75
+ LedgerError,
76
+ SubmissionLedger,
77
+ type SubmissionSnapshot,
78
+ type AdmissionRequest,
79
+ type SettlementFinalization,
80
+ type WorkerStopCommand,
81
+ } from "@yielded/agent/submission-ledger";
82
+ import { ThreadProjectionMaintenance } from "@yielded/agent/thread-projection-maintenance";
83
+ import { ThreadStoreError, ThreadStore } from "@yielded/agent/thread-store";
84
+ import { ToolReconciler } from "@yielded/agent/tool-reconciler";
85
+ import { type WakeScheduler } from "@yielded/agent/wake-scheduler";
86
+ import {
87
+ type Crypto,
88
+ Cause,
89
+ Clock,
90
+ Context,
91
+ Duration,
92
+ Effect,
93
+ Exit,
94
+ ErrorReporter,
95
+ Layer,
96
+ Match,
97
+ Schema,
98
+ Semaphore,
99
+ Option,
100
+ } from "effect";
101
+ import { SqlClient } from "effect/sql/SqlClient";
102
+
103
+ import {
104
+ type ThreadRecoveryEvents,
105
+ DurableAlarmError,
106
+ ThreadMaintenance,
107
+ ThreadNativeMaintenance,
108
+ ThreadMutationGate,
109
+ ThreadPublication,
110
+ publishCommitted,
111
+ ThreadMaintenanceFailpoint,
112
+ DurableAlarmService,
113
+ type ThreadMaintenanceFailpointHandler,
114
+ } from "../Alarm.ts";
115
+ import {
116
+ ThreadObjectIdentity,
117
+ ThreadObjectPlacement,
118
+ DurableObjectContext,
119
+ } from "../CloudflareBindings.ts";
120
+ import {
121
+ CLOUDFLARE_RUNTIME_DEFAULTS,
122
+ CloudflareDurableRuntimeConfig,
123
+ CloudflareDurableRuntimeConfigValue,
124
+ CloudflarePlatformConfigError,
125
+ } from "../CloudflareConfig.ts";
126
+ import { CloudflareThreadClient } from "../CloudflareThreadClient.ts";
127
+ import { cloudflareWakeSchedulerLayer } from "../WakeScheduler.ts";
128
+ import * as DueQueue from "./due-queue.ts";
129
+ import {
130
+ guardedMessageDeliveryStoreLayer,
131
+ threadMessageDeliveryLayer,
132
+ } from "./message-delivery.ts";
133
+ import { cloudflarePreparedInputAdmissionLayer } from "./prepared-admission.ts";
134
+ import { ProgressWaitRegistry } from "./progress-wait.ts";
135
+ import { threadPortTransportLayer } from "./transport.ts";
136
+
137
+ /**
138
+ * Raw (unvalidated) construction options for `ThreadObject.make`, mirroring
139
+ * `NodeDurableAgentRuntimeOptions`. Optional fields default to the documented production values
140
+ * (`CLOUDFLARE_RUNTIME_DEFAULTS`); everything is schema-decoded into
141
+ * `CloudflareDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
142
+ */
143
+ export interface CloudflareDurableRuntimeOptions {
144
+ readonly deploymentId: string;
145
+ /** Head of the minted producer identity `{producerPrefix}:{threadId}`. */
146
+ readonly producerPrefix: string;
147
+ /** Milliseconds; default 30s (D5). */
148
+ readonly ownershipLeaseDuration?: number | undefined;
149
+ /** Milliseconds; default 100. */
150
+ readonly alarmBackoffBase?: number | undefined;
151
+ /** Failed/no-progress exponential backoff ceiling in milliseconds; default 5000. */
152
+ readonly alarmBackoffCap?: number | undefined;
153
+ /** Milliseconds; default 500. */
154
+ readonly settlementPollInterval?: number | undefined;
155
+ /** Milliseconds; default 10000. */
156
+ readonly leaseRenewalInterval?: number | undefined;
157
+ /** Milliseconds; default 500. */
158
+ readonly abortPollInterval?: number | undefined;
159
+ /** Deployment-owned pricing authority used by durable cost budgets and settlements. */
160
+ readonly estimateCostMicrousd?: RunCostEstimator | undefined;
161
+ /** Closed trusted Tool failure reporting. Omission masks ambient observers at construction. */
162
+ readonly toolFailureObserver?: RunToolFailureObserver | undefined;
163
+ /** Milliseconds; default 25. */
164
+ readonly observationPollInterval?: number | undefined;
165
+ /** Whole disposable projection wave in milliseconds, 1..300000; default 30000. */
166
+ readonly projectionDispatchTimeoutMillis?: number | undefined;
167
+ /** Bytes; default just under the 2 MB platform value limit. */
168
+ readonly maxStoredValueBytes?: number | undefined;
169
+ /** Default false. */
170
+ readonly verifyOnOpen?: boolean | undefined;
171
+ /** Nonterminal Submissions per lane before admission refuses; default 256. */
172
+ readonly maxQueueDepthPerLane?: number | undefined;
173
+ /** Encoded input bytes per Submission; default = the stored-value bound. */
174
+ readonly maxInputBytes?: number | undefined;
175
+ /** `ctx.storage.sql.databaseSize` ceiling at admission; default 9 GB (10 GB platform cap). */
176
+ readonly maxDatabaseBytes?: number | undefined;
177
+ /**
178
+ * Durable Object storage fault injection (`ledger:*` / `append:*` locations). Handlers are
179
+ * constructed per incarnation WITH the live `DurableObjectState`, so eviction harnesses can
180
+ * map an armed hit to `ctx.abort()` — the platform's real failure mode. Default none.
181
+ */
182
+ readonly storageFailpoint?: ((ctx: DurableObjectState) => DoStorageFailpointHandler) | undefined;
183
+ /** Coordinator fault injection (`submit:*` / `terminalize:*` locations); default none. */
184
+ readonly runtimeFailpoint?:
185
+ | ((ctx: DurableObjectState) => DurableRuntimeFailpointHandler)
186
+ | undefined;
187
+ /** Thread-maintenance generation/alarm fault injection; default none. */
188
+ readonly maintenanceFailpoint?:
189
+ | ((ctx: DurableObjectState) => ThreadMaintenanceFailpointHandler)
190
+ | undefined;
191
+ /** Host-supplied fail-closed authorization policy; defaults to service possession. */
192
+ readonly operationAuthorizer?: OperationAuthorizerService | undefined;
193
+ /**
194
+ * Reconciliation policy consulted for open ordinary Tool Calls before an Unknown Outcome
195
+ * is recorded (durability §10, DUR-009). Defaults to the fail-closed
196
+ * `ToolReconciler.uncertain`.
197
+ */
198
+ readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
199
+ }
200
+
201
+ /** Services supplied before the application graph is built, including its dependencies. */
202
+ export type CloudflareBootstrapServices =
203
+ | CloudflareDurableRuntimeConfig
204
+ | ThreadObjectIdentity
205
+ | ThreadObjectPlacement
206
+ | DurableRuntimeConfig
207
+ | Crypto.Crypto
208
+ | DoStorageFailpoint
209
+ | DurableRuntimeFailpoint
210
+ | ThreadMaintenanceFailpoint
211
+ | RunContextPreparation
212
+ | RunToolAuthorization
213
+ | ToolReconciler;
214
+
215
+ /** Every construction failure of the assembled Cloudflare durable runtime stack. */
216
+ export type CloudflareDurableRuntimeInitializationError =
217
+ | CloudflarePlatformConfigError
218
+ | DigestError
219
+ | MessageDeliveryError
220
+ | DoStorageInitializationError;
221
+
222
+ /**
223
+ * The services `ThreadObject.layer` provides, including its single owner SQL client.
224
+ * Its Context also supplies the defaulted ThreadMessageDelivery and ThreadHostMaintenance
225
+ * references so rebuilt maintenance retains message recovery and lifecycle publication lanes.
226
+ */
227
+ export type CloudflareDurableRuntimeServices =
228
+ | DurableAgentRuntime
229
+ | SubmissionLedger
230
+ | ThreadStore
231
+ | MessageDeliveryStore
232
+ | WakeScheduler
233
+ | DurableAlarmService
234
+ | ThreadMaintenance
235
+ | ThreadMutationGate
236
+ | ThreadPublication
237
+ | ThreadProjectionMaintenance
238
+ | ThreadObjectPorts
239
+ | ProgressWaitRegistry
240
+ | SqlClient;
241
+
242
+ /**
243
+ * Owner-side execution port for a `portCall` request the wire endpoint has already decoded.
244
+ * It executes against THIS Object's LOCAL port facets — never the routed decorators, so a
245
+ * request cannot bounce between Objects — and returns the typed response for the endpoint to
246
+ * encode.
247
+ */
248
+ export class ThreadObjectPorts extends Context.Service<
249
+ ThreadObjectPorts,
250
+ {
251
+ readonly handle: (request: PortRequest) => Effect.Effect<PortResponse>;
252
+ /**
253
+ * Local-only lookup: encoded identities and returned rows must belong to this physical
254
+ * owner. Addressed RPCs additionally validate the exact logical Thread.
255
+ */
256
+ readonly lookupSubmission: (
257
+ submissionId: SubmissionId,
258
+ ) => Effect.Effect<Option.Option<SubmissionSnapshot>, LedgerError>;
259
+ }
260
+ >()("@effect-agent/platform-cloudflare/ThreadObjectPorts") {}
261
+
262
+ const decodeConfigValue = Schema.decodeUnknownEffect(CloudflareDurableRuntimeConfigValue);
263
+ const decodeThreadId = Schema.decodeUnknownEffect(ThreadId);
264
+ const decodeProducerId = Schema.decodeUnknownEffect(ProducerId);
265
+
266
+ const configFromOptions = (
267
+ options: CloudflareDurableRuntimeOptions,
268
+ ): Effect.Effect<CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError> =>
269
+ decodeConfigValue({
270
+ deploymentId: options.deploymentId,
271
+ producerPrefix: options.producerPrefix,
272
+ ownershipLeaseDuration:
273
+ options.ownershipLeaseDuration ?? CLOUDFLARE_RUNTIME_DEFAULTS.ownershipLeaseDuration,
274
+ alarmBackoffBase: options.alarmBackoffBase ?? CLOUDFLARE_RUNTIME_DEFAULTS.alarmBackoffBase,
275
+ alarmBackoffCap: options.alarmBackoffCap ?? CLOUDFLARE_RUNTIME_DEFAULTS.alarmBackoffCap,
276
+ settlementPollInterval:
277
+ options.settlementPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.settlementPollInterval,
278
+ leaseRenewalInterval:
279
+ options.leaseRenewalInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.leaseRenewalInterval,
280
+ abortPollInterval: options.abortPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.abortPollInterval,
281
+ observationPollInterval:
282
+ options.observationPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.observationPollInterval,
283
+ projectionDispatchTimeoutMillis:
284
+ options.projectionDispatchTimeoutMillis ??
285
+ CLOUDFLARE_RUNTIME_DEFAULTS.projectionDispatchTimeoutMillis,
286
+ maxStoredValueBytes:
287
+ options.maxStoredValueBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxStoredValueBytes,
288
+ verifyOnOpen: options.verifyOnOpen ?? CLOUDFLARE_RUNTIME_DEFAULTS.verifyOnOpen,
289
+ limits: {
290
+ maxQueueDepthPerLane:
291
+ options.maxQueueDepthPerLane ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxQueueDepthPerLane,
292
+ maxInputBytes: Math.min(
293
+ options.maxInputBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxInputBytes,
294
+ options.maxStoredValueBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxStoredValueBytes,
295
+ ),
296
+ maxDatabaseBytes: options.maxDatabaseBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxDatabaseBytes,
297
+ },
298
+ }).pipe(
299
+ Effect.mapError((error) =>
300
+ CloudflarePlatformConfigError.make({
301
+ message: `Invalid Cloudflare durable runtime configuration: ${error.message}`,
302
+ cause: error,
303
+ }),
304
+ ),
305
+ );
306
+
307
+ /**
308
+ * The Thread this Object owns, from the Object identity rule (plan §1.2): Thread
309
+ * Objects are addressed exclusively by `idFromName(threadId)`, so `ctx.id.name` IS the
310
+ * Thread ID. An unnamed Object (from `newUniqueId`) is a deployment error, not a lane.
311
+ */
312
+ const threadIdFromState = (
313
+ ctx: DurableObjectState,
314
+ ): Effect.Effect<ThreadId, CloudflarePlatformConfigError> =>
315
+ ctx.id.name === undefined
316
+ ? Effect.fail(
317
+ CloudflarePlatformConfigError.make({
318
+ message:
319
+ "This Durable Object was not created via idFromName(threadId); Thread " +
320
+ "Objects must be addressed by their Thread identity (plan §1.2).",
321
+ }),
322
+ )
323
+ : decodeThreadId(ctx.id.name).pipe(
324
+ Effect.mapError((error) =>
325
+ CloudflarePlatformConfigError.make({
326
+ message: `The Durable Object name is not a valid ThreadId: ${error.message}`,
327
+ cause: error,
328
+ }),
329
+ ),
330
+ );
331
+
332
+ /**
333
+ * Validate deployment settings and derive services before building application dependencies.
334
+ * The native class factory builds this Layer inside its constructor gate. Custom Effect hosts
335
+ * can provide it around the complete application Layer with the native context already supplied.
336
+ */
337
+ const runtimeConfigLayer = (
338
+ options: CloudflareDurableRuntimeOptions,
339
+ producerId: ProducerId,
340
+ ): Layer.Layer<
341
+ Exclude<CloudflareBootstrapServices, ThreadObjectIdentity | ThreadObjectPlacement>,
342
+ CloudflarePlatformConfigError,
343
+ DurableObjectContext
344
+ > =>
345
+ Layer.unwrap(
346
+ Effect.gen(function* () {
347
+ const { ctx } = yield* DurableObjectContext;
348
+ const config = yield* configFromOptions(options);
349
+
350
+ return Layer.mergeAll(
351
+ Layer.succeed(CloudflareDurableRuntimeConfig, config),
352
+ DurableRuntimeConfig.layer({
353
+ deploymentId: config.deploymentId,
354
+ producerId,
355
+ settlementPollInterval: Duration.millis(config.settlementPollInterval),
356
+ leaseRenewalInterval: Duration.millis(config.leaseRenewalInterval),
357
+ abortPollInterval: Duration.millis(config.abortPollInterval),
358
+ ...(options.estimateCostMicrousd === undefined
359
+ ? {}
360
+ : { estimateCostMicrousd: options.estimateCostMicrousd }),
361
+ }),
362
+ BrowserCrypto.layer,
363
+ storageFailpointLayer({ storage: ctx.storage, failpoint: options.storageFailpoint?.(ctx) }),
364
+ options.runtimeFailpoint === undefined
365
+ ? DurableRuntimeFailpoint.layer
366
+ : Layer.succeed(DurableRuntimeFailpoint, { hit: options.runtimeFailpoint(ctx) }),
367
+ options.maintenanceFailpoint === undefined
368
+ ? ThreadMaintenanceFailpoint.layer
369
+ : Layer.succeed(ThreadMaintenanceFailpoint, {
370
+ hit: options.maintenanceFailpoint(ctx),
371
+ }),
372
+ options.toolReconciler ?? ToolReconciler.uncertain,
373
+ options.operationAuthorizer === undefined
374
+ ? Layer.empty
375
+ : operationAuthorizerLayer(options.operationAuthorizer),
376
+ options.toolFailureObserver === undefined
377
+ ? Layer.succeed(CurrentToolFailureObserver, undefined)
378
+ : toolFailureObserverLayer(options.toolFailureObserver),
379
+ RunContextPreparationPassthrough,
380
+ RunToolAuthorization.allowAll,
381
+ );
382
+ }),
383
+ );
384
+
385
+ const producerIdentity = (prefix: string, owner: string) =>
386
+ decodeProducerId(`${prefix}:${owner}`).pipe(
387
+ Effect.mapError((error) =>
388
+ CloudflarePlatformConfigError.make({
389
+ message: `The minted producer identity is invalid: ${error.message}`,
390
+ cause: error,
391
+ }),
392
+ ),
393
+ );
394
+
395
+ /** Native one-Thread configuration; retains its existing producer and logical identities. */
396
+ export const layerConfig = (
397
+ options: CloudflareDurableRuntimeOptions,
398
+ ): Layer.Layer<CloudflareBootstrapServices, CloudflarePlatformConfigError, DurableObjectContext> =>
399
+ Layer.unwrap(
400
+ Effect.gen(function* () {
401
+ const { ctx } = yield* DurableObjectContext;
402
+ const threadId = yield* threadIdFromState(ctx);
403
+ const producerId = yield* producerIdentity(options.producerPrefix, threadId);
404
+
405
+ return Layer.mergeAll(
406
+ runtimeConfigLayer(options, producerId),
407
+ Layer.succeed(ThreadObjectIdentity, { threadId, producerId }),
408
+ Layer.succeed(ThreadObjectPlacement, { ownsThread: (target) => target === threadId }),
409
+ );
410
+ }),
411
+ );
412
+
413
+ /**
414
+ * Configuration for an application Object owning several logical Threads. The producer is
415
+ * the stable physical Object. No logical identity is installed globally: handleRpc binds and
416
+ * validates it per request using the placement guard and this actual runtime producer.
417
+ */
418
+ export const layerHostConfig = (
419
+ options: CloudflareDurableRuntimeOptions,
420
+ ownsThread: (threadId: ThreadId) => boolean,
421
+ ): Layer.Layer<
422
+ Exclude<CloudflareBootstrapServices, ThreadObjectIdentity>,
423
+ CloudflarePlatformConfigError,
424
+ DurableObjectContext
425
+ > =>
426
+ Layer.unwrap(
427
+ Effect.gen(function* () {
428
+ const { ctx } = yield* DurableObjectContext;
429
+ const producerId = yield* producerIdentity(options.producerPrefix, ctx.id.toString());
430
+
431
+ return Layer.merge(
432
+ runtimeConfigLayer(options, producerId),
433
+ Layer.succeed(ThreadObjectPlacement, { ownsThread }),
434
+ );
435
+ }),
436
+ );
437
+
438
+ /** Native source mutations which can create independently retained application obligations. */
439
+ export type ThreadHostMutation =
440
+ | { readonly _tag: "Admission"; readonly request: AdmissionRequest }
441
+ | { readonly _tag: "Settlement"; readonly request: SettlementFinalization }
442
+ | { readonly _tag: "WorkerStop"; readonly request: WorkerStopCommand };
443
+
444
+ export interface ThreadPublicationOptions<E = never, R = never, P = never> {
445
+ /** Pure, bounded selection of affected host lane IDs. The native owner prearms and guards
446
+ * these lanes through the source mutation, including replay and recovery-only finalization.
447
+ * Return only registered host IDs; do not perform I/O or acquire maintenance services here.
448
+ * Delivery is independent of lifecycle publication and remains at least once.
449
+ */
450
+ readonly hostLanesForMutation?: (mutation: ThreadHostMutation) => ReadonlyArray<string>;
451
+ /** Per-Submission recovery fault transitions. Capture host services once per incarnation;
452
+ * acknowledge only after durable application or outbox retention. Delivery runs in a
453
+ * separate bounded maintenance lane and does not gate native execution.
454
+ */
455
+ readonly recoveryEvents?: Layer.Layer<ThreadRecoveryEvents, E, R>;
456
+ /** Typed native facts, retained atomically and published asynchronously in owner batches.
457
+ * The handler returns after the application batch and its receipts commit. No journal
458
+ * scan is needed. The handler receives private native evidence, not pre-authorized UI data.
459
+ */
460
+ readonly lifecyclePublication?: Layer.Layer<LifecyclePublicationHandler, E, R>;
461
+ /**
462
+ * Optional host outbox consumer, built once per incarnation with RAW LOCAL ThreadStore and
463
+ * SubmissionLedger services. Yield DurableObjectContext and ThreadObjectIdentity for native
464
+ * bindings and identity. Initialization is local-only, inside the constructor gate; setup
465
+ * errors and additional requirements remain in the returned Layer. Layer.effect owns Scope.
466
+ * Canonical appends and durable approval, abort and unknown-resolution intents invalidate
467
+ * publication after commit. Custom host facts must use ThreadMaintenance.withMutation.
468
+ */
469
+ readonly publication?: Layer.Layer<ThreadPublication, E, R>;
470
+ /**
471
+ * Disposable index maintenance, built once with the raw local ThreadStore and owner
472
+ * SqlClient. Additional services P are exposed by the returned Layer, allowing Tool
473
+ * handlers and maintenance to share one index instance. Construction is local-only.
474
+ * Live committed batches run before append returns; bounded backfill uses the native
475
+ * alarm without delaying execution behind projection backlog.
476
+ */
477
+ readonly projection?: Layer.Layer<ThreadProjectionMaintenance | P, E, R>;
478
+ }
479
+
480
+ /**
481
+ * Register typed Agents and version declarations. Hashing and dependency capture happen in
482
+ * this Layer's Scope, after application Layers have been provided. Every Agent's instruction,
483
+ * Tool, Schema, and model requirements remain visible until satisfied by Layer composition.
484
+ * Use Layer.unwrap for registration values that need effectful application setup.
485
+ */
486
+ const registeredLayer = <
487
+ const Entries extends ReadonlyArray<AgentRegistration>,
488
+ E = never,
489
+ R = never,
490
+ >(
491
+ registrations: Entries,
492
+ options: ThreadPublicationOptions<E, R> = {},
493
+ ) =>
494
+ Layer.unwrap(
495
+ Effect.map(compileRegistrations(registrations), (bindings) => boundLayer(bindings, options)),
496
+ );
497
+
498
+ /** Preserve additional index services only when a projection Layer is actually supplied. */
499
+ export function layer<
500
+ const Entries extends ReadonlyArray<AgentRegistration>,
501
+ E = never,
502
+ R = never,
503
+ P = never,
504
+ PE = never,
505
+ PR = never,
506
+ >(
507
+ registrations: Entries,
508
+ options: Omit<ThreadPublicationOptions<E, R>, "projection"> & {
509
+ readonly projection: Layer.Layer<ThreadProjectionMaintenance | P, PE, PR>;
510
+ },
511
+ ): Layer.Layer<
512
+ Layer.Success<ReturnType<typeof registeredLayer<Entries, E | PE, R | PR>>> | P,
513
+ Layer.Error<ReturnType<typeof registeredLayer<Entries, E | PE, R | PR>>>,
514
+ Layer.Services<ReturnType<typeof registeredLayer<Entries, E | PE, R | PR>>>
515
+ >;
516
+
517
+ export function layer<const Entries extends ReadonlyArray<AgentRegistration>, E = never, R = never>(
518
+ registrations: Entries,
519
+ options?: ThreadPublicationOptions<E, R>,
520
+ ): ReturnType<typeof registeredLayer<Entries, E, R>>;
521
+
522
+ export function layer<const Entries extends ReadonlyArray<AgentRegistration>, E = never, R = never>(
523
+ registrations: Entries,
524
+ options: ThreadPublicationOptions<E, R> = {},
525
+ ) {
526
+ return registeredLayer(registrations, options);
527
+ }
528
+
529
+ export function layerFromBindings<E = never, R = never, P = never, PE = never, PR = never>(
530
+ bindings: ReadonlyArray<ResolvedBinding>,
531
+ options: Omit<ThreadPublicationOptions<E, R>, "projection"> & {
532
+ readonly projection: Layer.Layer<ThreadProjectionMaintenance | P, PE, PR>;
533
+ },
534
+ ): Layer.Layer<
535
+ CloudflareDurableRuntimeServices | P,
536
+ Layer.Error<ReturnType<typeof boundLayer<E | PE, R | PR>>>,
537
+ Layer.Services<ReturnType<typeof boundLayer<E | PE, R | PR>>>
538
+ >;
539
+
540
+ export function layerFromBindings<E = never, R = never>(
541
+ bindings: ReadonlyArray<ResolvedBinding>,
542
+ options?: ThreadPublicationOptions<E, R>,
543
+ ): ReturnType<typeof boundLayer<E, R>>;
544
+
545
+ export function layerFromBindings(
546
+ bindings: ReadonlyArray<ResolvedBinding>,
547
+ ): ReturnType<typeof boundLayer<never, never>>;
548
+
549
+ export function layerFromBindings<E = never, R = never>(
550
+ bindings: ReadonlyArray<ResolvedBinding>,
551
+ options: ThreadPublicationOptions<E, R> = {},
552
+ ) {
553
+ return boundLayer(bindings, options);
554
+ }
555
+
556
+ /**
557
+ * Assemble the durable runtime from already-resolved Agent Bindings.
558
+ * Use `ThreadObject.layer` to compile typed Agent registrations instead.
559
+ * Supply host services through `ThreadObject.make` or `ThreadObject.layerConfig` and
560
+ * the Durable Object context and namespace Layers when composing a custom host.
561
+ */
562
+ const boundLayer = <E = never, R = never>(
563
+ bindings: ReadonlyArray<ResolvedBinding>,
564
+ options: ThreadPublicationOptions<E, R> = {},
565
+ ) =>
566
+ Layer.unwrap(
567
+ Effect.map(DurableObjectContext, ({ ctx }) =>
568
+ sharedLayer(DurableAgentRuntime.layerWithBindings(bindings), options).pipe(
569
+ Layer.provideMerge(SqliteClient.layer({ storage: ctx.storage })),
570
+ ),
571
+ ),
572
+ );
573
+
574
+ /**
575
+ * Build the application runtime over the existing owner SqlClient and native ports, then
576
+ * assemble its one maintenance coordinator. The application may acquire its Bindings from
577
+ * those ports and expose extra services, including ThreadHostMaintenance. It must not acquire
578
+ * another runtime stack or require ThreadMaintenance while constructing this Layer.
579
+ * Supply layerHostConfig and deterministic placement; dispatch addressed ingress with handleRpc.
580
+ */
581
+ export function layerInHost<A, E, R, P = never, PE = never, PR = never>(
582
+ application: Layer.Layer<DurableAgentRuntime | A, E, R>,
583
+ options: Omit<ThreadPublicationOptions<PE, PR>, "projection"> & {
584
+ readonly projection: Layer.Layer<ThreadProjectionMaintenance | P, PE, PR>;
585
+ },
586
+ ): Layer.Layer<
587
+ CloudflareDurableRuntimeServices | A | P,
588
+ Layer.Error<ReturnType<typeof sharedLayer<A, E, R, PE, PR>>>,
589
+ Layer.Services<ReturnType<typeof sharedLayer<A, E, Exclude<R, P>, PE, PR>>>
590
+ >;
591
+
592
+ export function layerInHost<A, E, R, PE = never, PR = never>(
593
+ application: Layer.Layer<DurableAgentRuntime | A, E, R>,
594
+ options?: ThreadPublicationOptions<PE, PR>,
595
+ ): ReturnType<typeof sharedLayer<A, E, R, PE, PR>>;
596
+
597
+ export function layerInHost<A, E, R, PE = never, PR = never>(
598
+ application: Layer.Layer<DurableAgentRuntime | A, E, R>,
599
+ options: ThreadPublicationOptions<PE, PR> = {},
600
+ ) {
601
+ return sharedLayer(application, options);
602
+ }
603
+
604
+ const sharedLayer = <A, E, R, PE = never, PR = never>(
605
+ application: Layer.Layer<DurableAgentRuntime | A, E, R>,
606
+ options: ThreadPublicationOptions<PE, PR> = {},
607
+ ) =>
608
+ Layer.unwrap(
609
+ Effect.gen(function* () {
610
+ const { ctx } = yield* DurableObjectContext;
611
+ const config = yield* CloudflareDurableRuntimeConfig;
612
+ const { ownsThread } = yield* ThreadObjectPlacement;
613
+
614
+ const storageOptions: DoStorageOptions = {
615
+ storage: ctx.storage,
616
+ observationPollInterval: config.observationPollInterval,
617
+ ownershipLeaseDuration: config.ownershipLeaseDuration,
618
+ maxStoredValueBytes: config.maxStoredValueBytes,
619
+ verifyOnOpen: config.verifyOnOpen,
620
+ };
621
+
622
+ const infrastructure = Layer.mergeAll(
623
+ options.lifecyclePublication === undefined ? Layer.empty : lifecyclePublicationLayer,
624
+ storageConfigLayer(storageOptions),
625
+ Layer.effect(SqlClient)(SqlClient),
626
+ );
627
+
628
+ // Preserve the database owner's reader gate and rollback invalidators, while
629
+ // making source transactions explicitly own the due-queue flush.
630
+ const sourceOwner = Layer.effect(SqlStorageOwner)(
631
+ Effect.gen(function* () {
632
+ const owner = yield* SqlStorageOwner;
633
+ const mutations = yield* ThreadMutationGate;
634
+
635
+ if (owner === undefined) return yield* Effect.die(new Error("SQL owner unavailable"));
636
+
637
+ return {
638
+ identity: owner.identity ?? owner,
639
+ read: owner.read,
640
+ invalidators: owner.invalidators,
641
+ transaction: <A, E, R>(body: Effect.Effect<A, E, R>) =>
642
+ owner.read(
643
+ mutations.withTransaction(body).pipe(
644
+ Effect.onExit((exit) =>
645
+ Exit.isFailure(exit)
646
+ ? Effect.sync(() => {
647
+ for (const invalidate of owner.invalidators) invalidate();
648
+ })
649
+ : Effect.void,
650
+ ),
651
+ ),
652
+ ),
653
+ };
654
+ }),
655
+ ).pipe(Layer.provide(sqlOwnerLayer));
656
+
657
+ const localInfrastructure = Layer.mergeAll(infrastructure, sourceOwner);
658
+
659
+ const sourceProgress = Layer.effect(SqlStorageProgress)(
660
+ Effect.gen(function* () {
661
+ const mutations = yield* ThreadMutationGate;
662
+
663
+ return {
664
+ committed: (kind) => {
665
+ const lanes = Match.value(kind).pipe(
666
+ Match.when("canonical", () => [
667
+ DueQueue.Native,
668
+ DueQueue.Publication,
669
+ ...(options.projection === undefined ? [] : [DueQueue.Projection]),
670
+ ]),
671
+ Match.whenOr("submission", "control", () => [
672
+ DueQueue.Native,
673
+ DueQueue.Publication,
674
+ ]),
675
+ Match.when("delivery", () => [
676
+ DueQueue.Messages,
677
+ ...(options.lifecyclePublication === undefined ? [] : [DueQueue.Lifecycle]),
678
+ ]),
679
+ Match.whenOr("lifecycle", "lifecycle-ack", () =>
680
+ options.lifecyclePublication === undefined ? [] : [DueQueue.Lifecycle],
681
+ ),
682
+ Match.exhaustive,
683
+ );
684
+
685
+ return mutations.recordProgress(lanes).pipe(
686
+ Effect.catchCause((cause) =>
687
+ Effect.failCause(
688
+ Cause.map(cause, (failure) =>
689
+ SqlStorageProgressError.make({
690
+ operation: "record maintenance progress",
691
+ message: "Committed source work could not enroll maintenance",
692
+ cause: failure,
693
+ }),
694
+ ),
695
+ ),
696
+ ),
697
+ );
698
+ },
699
+ };
700
+ }),
701
+ );
702
+
703
+ // The same local ports serve routed decorators and owner-side RPC execution.
704
+ // The RPC executor must never receive routed ports and bounce requests between Objects.
705
+ const rawLocalPorts = Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(
706
+ Layer.provide(localInfrastructure),
707
+ Layer.provide(sourceProgress),
708
+ );
709
+
710
+ const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
711
+ const wakes = cloudflareWakeSchedulerLayer.pipe(Layer.provide(base));
712
+
713
+ const messageStore = guardedMessageDeliveryStoreLayer.pipe(
714
+ Layer.provide(
715
+ doMessageDeliveryStoreLayer().pipe(
716
+ Layer.provide(localInfrastructure),
717
+ Layer.provide(sourceProgress),
718
+ ),
719
+ ),
720
+ Layer.provide(wakes),
721
+ );
722
+
723
+ const messageRecovery = threadMessageDeliveryLayer.pipe(
724
+ // Four parallel attempts per wave. Native completion stops new wake-driven waves;
725
+ // retained retry deadlines schedule future alarms.
726
+ Layer.provide(MessageDeliveryDriver.layer({ batchSize: 4, concurrency: 4 })),
727
+ Layer.provide(cloudflarePreparedInputAdmissionLayer),
728
+ Layer.provide(CloudflareThreadClient.layer),
729
+ Layer.provide(messageStore),
730
+ Layer.provide(wakes),
731
+ );
732
+
733
+ const publication = (options.publication ?? ThreadPublication.layer).pipe(
734
+ Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
735
+ );
736
+
737
+ const projection = (options.projection ?? ThreadProjectionMaintenance.layer).pipe(
738
+ Layer.provide(Layer.mergeAll(rawLocalPorts, Layer.effect(SqlClient)(SqlClient))),
739
+ );
740
+
741
+ const localPorts =
742
+ options.publication === undefined &&
743
+ options.projection === undefined &&
744
+ options.hostLanesForMutation === undefined &&
745
+ options.lifecyclePublication === undefined
746
+ ? rawLocalPorts
747
+ : Layer.effectContext(
748
+ Effect.gen(function* () {
749
+ const store = yield* ThreadStore;
750
+ const ledger = yield* SubmissionLedger;
751
+ const mutations = yield* ThreadMutationGate;
752
+ const index = yield* ThreadProjectionMaintenance;
753
+ const publish = yield* Effect.context<ThreadPublication>();
754
+ const afterCommit = publishCommitted.pipe(Effect.provide(publish));
755
+ // A later append cannot mistake its still-projecting predecessor for old backlog.
756
+ // Publication remains outside this local source/index critical section.
757
+ const sourceCommits = yield* Semaphore.make(1);
758
+
759
+ // Every runtime-owned producer prearms too: a crash between commit and invalidation
760
+ // leaves a NEW, uncertified generation. Source errors keep their native port types.
761
+ const observedStore = ThreadStore.of({
762
+ ...store,
763
+ ...(store.lifecyclePublications === undefined
764
+ ? {}
765
+ : {
766
+ lifecyclePublications: {
767
+ ...store.lifecyclePublications,
768
+ retryParked: (ownerThreadId, nowMillis) =>
769
+ mutations
770
+ .withMutation(
771
+ store.lifecyclePublications!.retryParked(ownerThreadId, nowMillis),
772
+ { invalidatesRecovery: false, lanes: [DueQueue.Lifecycle] },
773
+ )
774
+ .pipe(
775
+ Effect.catchTag("DurableAlarmError", (cause) =>
776
+ LifecyclePublicationError.make({ reason: "unavailable", cause }),
777
+ ),
778
+ ),
779
+ },
780
+ }),
781
+ append: (request) =>
782
+ mutations
783
+ .withMutation(
784
+ sourceCommits
785
+ .withPermit(
786
+ store
787
+ .append(request)
788
+ .pipe(
789
+ Effect.tap((result) =>
790
+ index
791
+ .applyCommitted(request, result)
792
+ .pipe(
793
+ Effect.catchCause((cause) =>
794
+ Cause.hasInterrupts(cause)
795
+ ? Effect.interrupt
796
+ : Effect.logError(
797
+ "Thread projection deferred after source commit",
798
+ cause,
799
+ ),
800
+ ),
801
+ ),
802
+ ),
803
+ ),
804
+ )
805
+ .pipe(Effect.tap(() => afterCommit)),
806
+ {
807
+ lanes: [
808
+ ...(options.projection === undefined ? [] : [DueQueue.Projection]),
809
+ ...(options.lifecyclePublication === undefined ||
810
+ !request.batch.records.some((record) =>
811
+ Schema.is(Schema.toType(LifecyclePublicationFact))(record.payload),
812
+ )
813
+ ? []
814
+ : [DueQueue.Lifecycle]),
815
+ ],
816
+ },
817
+ )
818
+ .pipe(
819
+ Effect.catchTag("DurableAlarmError", (cause) =>
820
+ ThreadStoreError.make({
821
+ operation: "prearm publication append",
822
+ message: cause.message,
823
+ cause,
824
+ }),
825
+ ),
826
+ ),
827
+ });
828
+
829
+ const observeIntent = <A, Failure>(
830
+ body: Effect.Effect<A, Failure>,
831
+ invalidatesRecovery = true,
832
+ hostLanes: ReadonlyArray<string> = [],
833
+ ) =>
834
+ mutations
835
+ .withMutation(
836
+ body.pipe(
837
+ Effect.tap(() => (invalidatesRecovery ? afterCommit : Effect.void)),
838
+ ),
839
+ {
840
+ invalidatesRecovery,
841
+ lanes: [
842
+ ...hostLanes,
843
+ ...(options.lifecyclePublication === undefined
844
+ ? []
845
+ : [DueQueue.Lifecycle]),
846
+ ],
847
+ },
848
+ )
849
+ .pipe(
850
+ Effect.catchTag("DurableAlarmError", (cause) =>
851
+ LedgerError.make({
852
+ operation: "prearm publication intent",
853
+ message: "The publication generation could not be armed",
854
+ cause,
855
+ }),
856
+ ),
857
+ );
858
+
859
+ const observeMutation = <A, Failure>(
860
+ mutation: ThreadHostMutation,
861
+ body: Effect.Effect<A, Failure>,
862
+ invalidatesRecovery: boolean,
863
+ ) =>
864
+ Effect.suspend(() =>
865
+ Schema.decodeEffect(Schema.Array(DueQueue.HostLaneId))([
866
+ ...new Set(options.hostLanesForMutation?.(mutation) ?? []),
867
+ ]),
868
+ ).pipe(
869
+ Effect.mapError((cause) =>
870
+ LedgerError.make({
871
+ operation: "select host maintenance lanes",
872
+ message: "Native mutation selected an invalid host lane ID",
873
+ cause,
874
+ }),
875
+ ),
876
+ Effect.flatMap((lanes) => observeIntent(body, invalidatesRecovery, lanes)),
877
+ );
878
+
879
+ const stopWorker = ledger.stopWorker;
880
+
881
+ return Context.make(ThreadStore, observedStore).pipe(
882
+ Context.add(SubmissionLedger, {
883
+ ...ledger,
884
+ ...(options.hostLanesForMutation === undefined
885
+ ? {}
886
+ : {
887
+ admit: (request: AdmissionRequest) =>
888
+ observeMutation(
889
+ { _tag: "Admission", request },
890
+ ledger.admit(request),
891
+ false,
892
+ ),
893
+ }),
894
+ ...(options.lifecyclePublication === undefined
895
+ ? {}
896
+ : {
897
+ markReady: (request) => observeIntent(ledger.markReady(request), false),
898
+ suspend: (request) => observeIntent(ledger.suspend(request), false),
899
+ markUnknown: (request) =>
900
+ observeIntent(ledger.markUnknown(request), false),
901
+ recordChildSettled: (request) =>
902
+ observeIntent(ledger.recordChildSettled(request), false),
903
+ }),
904
+ ...(options.lifecyclePublication === undefined &&
905
+ options.hostLanesForMutation === undefined
906
+ ? {}
907
+ : {
908
+ finalizeSettlement: (request: SettlementFinalization) =>
909
+ observeMutation(
910
+ { _tag: "Settlement", request },
911
+ ledger.finalizeSettlement(request),
912
+ false,
913
+ ),
914
+ }),
915
+ recordApprovalDecision: (request) =>
916
+ observeIntent(ledger.recordApprovalDecision(request)),
917
+ ...(stopWorker === undefined
918
+ ? {}
919
+ : {
920
+ stopWorker: (request: Parameters<NonNullable<typeof stopWorker>>[0]) =>
921
+ observeMutation(
922
+ { _tag: "WorkerStop", request },
923
+ stopWorker(request),
924
+ true,
925
+ ),
926
+ }),
927
+ requestAbort: (request) => observeIntent(ledger.requestAbort(request)),
928
+ recordUnknownResolution: (request) =>
929
+ observeIntent(ledger.recordUnknownResolution(request)),
930
+ }),
931
+ );
932
+ }),
933
+ ).pipe(Layer.provide(rawLocalPorts));
934
+
935
+ const portsEndpointLayer = Layer.effect(ThreadObjectPorts)(
936
+ Effect.gen(function* () {
937
+ const local = yield* Effect.context<
938
+ | SubmissionLedger
939
+ | ThreadStore
940
+ | MessageDeliveryStore
941
+ | WakeScheduler
942
+ | DurableRuntimeFailpoint
943
+ >();
944
+
945
+ const lookupSubmission = makeLocalSubmissionLookup({ ownsThread });
946
+
947
+ return ThreadObjectPorts.of({
948
+ handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
949
+ lookupSubmission: (submissionId) =>
950
+ lookupSubmission(submissionId).pipe(Effect.provide(local)),
951
+ });
952
+ }),
953
+ ).pipe(Layer.provide(localPorts), Layer.provide(messageStore), Layer.provide(wakes));
954
+
955
+ const routedPorts = Layer.mergeAll(
956
+ routedSubmissionLedgerLayer({ ownsThread }),
957
+ routedThreadStoreLayer({ ownsThread }),
958
+ routedWorkerAdmissionLayer({ ownsThread }),
959
+ ).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
960
+
961
+ const routedMessages = routedMessageDeliveryStoreLayer({ ownsThread }).pipe(
962
+ Layer.provide(messageStore),
963
+ Layer.provide(threadPortTransportLayer),
964
+ );
965
+
966
+ const runtimeStack = application.pipe(
967
+ Layer.provide(
968
+ cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
969
+ ),
970
+ Layer.provideMerge(routedMessages),
971
+ Layer.provideMerge(routedPorts),
972
+ Layer.provideMerge(wakes),
973
+ Layer.provideMerge(base),
974
+ Layer.provideMerge(portsEndpointLayer),
975
+ );
976
+
977
+ const maintenanceRuntime =
978
+ options.lifecyclePublication === undefined
979
+ ? runtimeStack
980
+ : Layer.merge(
981
+ runtimeStack,
982
+ Layer.effect(ThreadNativeMaintenance)(
983
+ Effect.gen(function* () {
984
+ const previous = yield* ThreadNativeMaintenance;
985
+ const store = yield* ThreadStore;
986
+ const storage = store.lifecyclePublications;
987
+
988
+ const context = yield* Effect.context<
989
+ LifecyclePublicationHandler | ThreadStore | SubmissionLedger
990
+ >();
991
+
992
+ const failure = (cause: unknown) => {
993
+ const error = DurableAlarmError.make({
994
+ operation: "publish native lifecycle",
995
+ message: "Native lifecycle publication remains pending",
996
+ cause,
997
+ });
998
+
999
+ return ErrorReporter.isIgnored(cause)
1000
+ ? Object.assign(error, { [ErrorReporter.ignore]: true })
1001
+ : error;
1002
+ };
1003
+
1004
+ const deadline = (
1005
+ storage === undefined
1006
+ ? Effect.fail(failure("Native lifecycle storage unavailable"))
1007
+ : storage.pendingDeadline.pipe(Effect.mapError(failure))
1008
+ ).pipe(
1009
+ Effect.withErrorReporting,
1010
+ Effect.catchCauseIf(
1011
+ (cause) => !Cause.hasInterrupts(cause),
1012
+ (cause) =>
1013
+ Effect.logError("Lifecycle publication deadline unavailable", cause).pipe(
1014
+ Effect.andThen(
1015
+ Effect.map(Clock.currentTimeMillis, (now) => Option.some(now + 60_000)),
1016
+ ),
1017
+ ),
1018
+ ),
1019
+ );
1020
+
1021
+ return {
1022
+ lanes: [
1023
+ ...previous.lanes,
1024
+ {
1025
+ id: DueQueue.Lifecycle,
1026
+ // Source commits enroll one concurrent lane. Materialize a bounded
1027
+ // journal suffix per wave; never wait for publication in native work.
1028
+ // Four owner batches, each with a 10s host timeout, plus local commits/cleanup.
1029
+ dispatchTimeoutMillis: 60_000,
1030
+ run:
1031
+ storage === undefined
1032
+ ? Effect.fail(failure("Native lifecycle storage unavailable"))
1033
+ : drainLifecyclePublications(storage).pipe(
1034
+ Effect.provide(context),
1035
+ Effect.mapError(failure),
1036
+ Effect.andThen(deadline),
1037
+ ),
1038
+ },
1039
+ ],
1040
+ };
1041
+ }),
1042
+ ).pipe(
1043
+ Layer.provide(rawLocalPorts),
1044
+ Layer.provide(options.lifecyclePublication.pipe(Layer.provide(rawLocalPorts))),
1045
+ Layer.provide(runtimeStack),
1046
+ ),
1047
+ );
1048
+
1049
+ return Layer.mergeAll(
1050
+ maintenanceRuntime,
1051
+ ThreadMaintenance.layer.pipe(
1052
+ Layer.provide(options.recoveryEvents ?? Layer.empty),
1053
+ Layer.provide(maintenanceRuntime),
1054
+ Layer.provide(messageRecovery),
1055
+ ),
1056
+ portsEndpointLayer,
1057
+ routedMessages,
1058
+ messageRecovery,
1059
+ ).pipe(
1060
+ Layer.provideMerge(sourceProgress),
1061
+ Layer.provideMerge(publication),
1062
+ Layer.provideMerge(projection),
1063
+ Layer.provideMerge(sourceOwner),
1064
+ Layer.provideMerge(ThreadMutationGate.layer),
1065
+ Layer.provideMerge(infrastructure),
1066
+ );
1067
+ }),
1068
+ );