@effect-agent/platform-cloudflare 0.1.0-beta.6 → 0.1.0-beta.60

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 (95) hide show
  1. package/dist/Alarm.d.mts +168 -0
  2. package/dist/Alarm.mjs +415 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserRestCapture.d.mts +35 -0
  5. package/dist/BrowserRestCapture.mjs +238 -0
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/BrowserRestCrawl.d.mts +17 -0
  8. package/dist/BrowserRestCrawl.mjs +244 -0
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/CloudflareBindings.d.mts +105 -0
  11. package/dist/CloudflareBindings.mjs +98 -0
  12. package/dist/CloudflareBindings.mjs.map +1 -0
  13. package/dist/CloudflareBrowser-Bj22nNUT.mjs +466 -0
  14. package/dist/CloudflareBrowser-Bj22nNUT.mjs.map +1 -0
  15. package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
  16. package/dist/CloudflareBrowser.d.mts +2 -0
  17. package/dist/CloudflareBrowser.mjs +2 -0
  18. package/dist/CloudflareCodeMode.d.mts +44 -0
  19. package/dist/CloudflareCodeMode.mjs +611 -0
  20. package/dist/CloudflareCodeMode.mjs.map +1 -0
  21. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  22. package/dist/CloudflareConfig.d.mts +2 -0
  23. package/dist/CloudflareConfig.mjs +122 -0
  24. package/dist/CloudflareConfig.mjs.map +1 -0
  25. package/dist/CloudflareMemory.d.mts +140 -0
  26. package/dist/CloudflareMemory.mjs +168 -0
  27. package/dist/CloudflareMemory.mjs.map +1 -0
  28. package/dist/CloudflareScheduling.d.mts +52 -0
  29. package/dist/CloudflareScheduling.mjs +389 -0
  30. package/dist/CloudflareScheduling.mjs.map +1 -0
  31. package/dist/CloudflareSubscriptions.d.mts +87 -0
  32. package/dist/CloudflareSubscriptions.mjs +524 -0
  33. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  34. package/dist/CloudflareThreadClient.d.mts +1214 -0
  35. package/dist/CloudflareThreadClient.mjs +386 -0
  36. package/dist/CloudflareThreadClient.mjs.map +1 -0
  37. package/dist/InteractiveBrowser.d.mts +144 -0
  38. package/dist/InteractiveBrowser.mjs +1090 -0
  39. package/dist/InteractiveBrowser.mjs.map +1 -0
  40. package/dist/ProtectedBrowser.d.mts +65 -0
  41. package/dist/ProtectedBrowser.mjs +859 -0
  42. package/dist/ProtectedBrowser.mjs.map +1 -0
  43. package/dist/ThreadObject-CvnEnVQH.d.mts +772 -0
  44. package/dist/ThreadObject-s5w3V_co.mjs +675 -0
  45. package/dist/ThreadObject-s5w3V_co.mjs.map +1 -0
  46. package/dist/ThreadObject.d.mts +2 -0
  47. package/dist/ThreadObject.mjs +3 -0
  48. package/dist/WakeScheduler.d.mts +23 -0
  49. package/dist/WakeScheduler.mjs +60 -0
  50. package/dist/WakeScheduler.mjs.map +1 -0
  51. package/dist/boundary-BguazVkh.mjs +42 -0
  52. package/dist/boundary-BguazVkh.mjs.map +1 -0
  53. package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
  54. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  55. package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
  56. package/dist/index.d.mts +12 -1543
  57. package/dist/index.mjs +12 -1636
  58. package/dist/prepared-admission-G7N4DDqS.mjs +73 -0
  59. package/dist/prepared-admission-G7N4DDqS.mjs.map +1 -0
  60. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  61. package/package.json +1 -53
  62. package/src/Alarm.ts +862 -0
  63. package/src/BrowserRestCapture.ts +477 -0
  64. package/src/BrowserRestCrawl.ts +540 -0
  65. package/src/CloudflareBindings.ts +171 -0
  66. package/src/CloudflareBrowser.ts +15 -0
  67. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +367 -204
  68. package/src/{config.ts → CloudflareConfig.ts} +9 -8
  69. package/src/CloudflareMemory.ts +359 -0
  70. package/src/CloudflareScheduling.ts +733 -0
  71. package/src/CloudflareSubscriptions.ts +1045 -0
  72. package/src/CloudflareThreadClient.ts +807 -0
  73. package/src/InteractiveBrowser.ts +2357 -0
  74. package/src/ProtectedBrowser.ts +2 -0
  75. package/src/{conversation-object.ts → ThreadObject.ts} +423 -239
  76. package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
  77. package/src/index.ts +11 -23
  78. package/src/internal/boundary.ts +55 -0
  79. package/src/internal/browser-quick-action.ts +818 -0
  80. package/src/internal/browser-session-lifecycle.ts +160 -0
  81. package/src/internal/layers.ts +615 -0
  82. package/src/internal/message-delivery.ts +90 -0
  83. package/src/internal/prepared-admission.ts +116 -0
  84. package/src/internal/progress-wait.ts +121 -0
  85. package/src/internal/transport.ts +42 -0
  86. package/src/protected-browser/binding.ts +187 -0
  87. package/src/protected-browser/inspect-frame.ts +125 -0
  88. package/src/protected-browser/native.ts +429 -0
  89. package/src/protected-browser/policy.ts +719 -0
  90. package/dist/index.mjs.map +0 -1
  91. package/src/alarm.ts +0 -334
  92. package/src/bindings.ts +0 -145
  93. package/src/client.ts +0 -646
  94. package/src/layers.ts +0 -376
  95. package/src/transport.ts +0 -38
@@ -1,35 +1,49 @@
1
- import type { AgentId } from "@effect-agent/core";
2
- import { SubmissionId } from "@effect-agent/core";
1
+ import { type AgentId } from "@effect-agent/core/Identifiers";
2
+ import { SubmissionId } from "@effect-agent/core/Identifiers";
3
+ import {
4
+ decodePortRequest,
5
+ encodePortResponse,
6
+ type PortRequest,
7
+ } from "@effect-agent/storage-cloudflare/PortProtocol";
3
8
  import {
4
- AppendConflict,
5
- ConversationNotMaterialized,
6
- ConversationRead,
7
- ConversationStore,
8
- ConversationStoreError,
9
- DigestError,
10
- DurableAgentRuntime,
11
- DurableRuntimeFailpointError,
12
- FenceRejected,
13
9
  IntegrityReport,
14
- LedgerError,
15
10
  ObligationReport,
16
11
  ObligationThresholds,
12
+ RecoveryExplanation,
13
+ RetryCommand,
14
+ RetryRefused,
15
+ } from "@effect-agent/thread/Admin";
16
+ import { DigestError } from "@effect-agent/thread/Digest";
17
+ import {
18
+ DurableAgentRuntime,
19
+ RecoveryReport,
20
+ type DurableSubmitAgent,
21
+ } from "@effect-agent/thread/DurableAgentRuntime";
22
+ import { DurableRuntimeFailpointError } from "@effect-agent/thread/DurableFailpoint";
23
+ import {
17
24
  OperationAuthorizationRequest,
18
25
  OperationAuthorizer,
19
26
  OperationDenied,
27
+ } from "@effect-agent/thread/OperationAuthorizer";
28
+ import { PersistedJson } from "@effect-agent/thread/Records";
29
+ import { RunJournalError } from "@effect-agent/thread/RunJournal";
30
+ import {
31
+ AdmissionPolicyError,
32
+ LedgerError,
20
33
  OwnershipLost,
21
- PersistedJson,
22
- RecoveryExplanation,
23
- RecoveryReport,
24
- RetryCommand,
25
- RetryRefused,
26
- RunJournalError,
27
34
  SettlementConflict,
28
35
  SubmissionLedger,
29
36
  SubmissionLookupByKey,
30
- type DurableSubmitAgent,
31
- } from "@effect-agent/session";
32
- import type { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
37
+ } from "@effect-agent/thread/SubmissionLedger";
38
+ import {
39
+ AppendConflict,
40
+ ThreadNotMaterialized,
41
+ ThreadRead,
42
+ ThreadStore,
43
+ ThreadStoreError,
44
+ FenceRejected,
45
+ } from "@effect-agent/thread/ThreadStore";
46
+ import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
33
47
  import { Effect, Layer, Option, Schema, Stream } from "effect";
34
48
  import {
35
49
  DurableObject as EffectCfDurableObject,
@@ -38,29 +52,35 @@ import {
38
52
  } from "effect-cf";
39
53
 
40
54
  import {
41
- ConversationMaintenance,
55
+ ThreadMaintenance,
42
56
  DurableAlarmError,
43
57
  DurableAlarmService,
44
58
  type MaintenancePassFailure,
45
- } from "./alarm.ts";
59
+ } from "./Alarm.ts";
46
60
  import {
47
- ConversationObjectIdentity,
61
+ ThreadObjectIdentity,
48
62
  DurableObjectContext,
49
- ConversationObjectNamespace,
50
- conversationNamespaceFromEnv,
63
+ ThreadObjectNamespace,
64
+ threadNamespaceFromEnv,
51
65
  type CloudflareBindingError,
52
- } from "./bindings.ts";
66
+ } from "./CloudflareBindings.ts";
67
+ import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.ts";
53
68
  import {
54
69
  AbortRecorded,
55
70
  ApprovalRecorded,
56
71
  HostFailed,
57
72
  HostProtocolError,
58
73
  ObservedPage,
74
+ ProgressObserved,
75
+ ProgressCancelled,
59
76
  SettlementReached,
77
+ SubmissionStatusResponse,
60
78
  SubmitSucceeded,
61
79
  UnknownResolutionRecorded,
62
80
  boundHostDiagnostic,
63
81
  decodeAbortCommand,
82
+ decodeAwaitProgressRequest,
83
+ decodeCancelProgressRequest,
64
84
  decodeApprovalDecisionCommand,
65
85
  decodeObservePageRequest,
66
86
  decodeReceipt,
@@ -69,25 +89,39 @@ import {
69
89
  encodeHostResponse,
70
90
  type HostFailure,
71
91
  type HostResponse,
72
- } from "./client.ts";
73
- import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./config.ts";
92
+ } from "./CloudflareThreadClient.ts";
74
93
  import {
75
- CloudflareDurableRuntime,
76
- ConversationObjectPorts,
94
+ layerConfig,
95
+ ThreadObjectPorts,
77
96
  type CloudflareDurableRuntimeInitializationError,
78
97
  type CloudflareDurableRuntimeOptions,
79
98
  type CloudflareDurableRuntimeServices,
80
- } from "./layers.ts";
99
+ type CloudflareBootstrapServices,
100
+ } from "./internal/layers.ts";
101
+ import { ProgressWaitRegistry } from "./internal/progress-wait.ts";
102
+
103
+ export {
104
+ layer,
105
+ layerConfig,
106
+ type ThreadPublicationOptions as PublicationOptions,
107
+ type CloudflareDurableRuntimeOptions as RuntimeOptions,
108
+ type CloudflareDurableRuntimeServices as Services,
109
+ type CloudflareDurableRuntimeInitializationError as InitializationError,
110
+ type CloudflareBootstrapServices as BootstrapServices,
111
+ } from "./internal/layers.ts";
81
112
 
82
113
  /**
83
- * `makeConversationObjectClass(options, observability?)` — the Conversation Durable Object
114
+ * `ThreadObject.make(application, options)` — the Thread Durable Object
84
115
  * (plan §1.4,
85
116
  * D-P6-1): a factory returning a class that applications export from their Worker entry.
86
- * One SQLite-backed Object per Conversation is the serialized owner (durability §6); the
117
+ * One SQLite-backed Object per Thread is the serialized owner (durability §6); the
87
118
  * Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
88
- * ONE bounded `runRecovery` + `processConversationResolved` pass, and the persisted alarm
119
+ * ONE bounded `runRecovery` + `processThreadHead` pass, and the persisted alarm
89
120
  * (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
90
121
  * incoming request.
122
+ * `Services` exposes the same owner `SqlClient` used by the Thread stores. Compose optional
123
+ * local repositories after `ThreadObject.layer`; never acquire another independently locked
124
+ * SQL client for the same Object. Exposing the client installs no additional storage schemas.
91
125
  *
92
126
  * Constructor gate (`blockConcurrencyWhile`) is LOCAL-ONLY: schema migration and the
93
127
  * exact-version check, configuration decode, and the defensive ensure-alarm half of the
@@ -97,39 +131,62 @@ import {
97
131
  * `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
98
132
  */
99
133
 
100
- /** Construction options for one deployed Conversation Object class. */
101
- export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptions {
134
+ /** Construction options for one deployed Thread Object class. */
135
+ export interface Options<
136
+ ApplicationServices = never,
137
+ EventServices = never,
138
+ EventLayerError = never,
139
+ > extends CloudflareDurableRuntimeOptions {
140
+ /** Accept transient native RPC tracing through effect-cf; disabled by default. */
141
+ readonly rpcTracing?: boolean;
102
142
  /**
103
143
  * Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
104
- * Object's route back to sibling Conversation Objects for the WP2 cross-Object port calls
144
+ * Object's route back to sibling Thread Objects for the WP2 cross-Object port calls
105
145
  * and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
106
146
  */
107
147
  readonly namespaceBinding: string;
148
+ /** Acquired and finalized per native event, with access to the complete application runtime. */
149
+ readonly eventLayer?: Layer.Layer<
150
+ EventServices,
151
+ EventLayerError,
152
+ | RuntimeServices
153
+ | ApplicationServices
154
+ | EffectCfDurableObjectState.DurableObjectState
155
+ | WorkerEnvironment
156
+ >;
108
157
  }
109
158
 
110
- type EndpointServices = CloudflareDurableRuntimeServices | DurableObjectContext;
111
- type RuntimeServices = EndpointServices | ConversationObjectNamespace;
112
- type ConversationObjectInitializationError =
159
+ type EndpointServices =
160
+ | CloudflareDurableRuntimeServices
161
+ | CloudflareBootstrapServices
162
+ | DurableObjectContext;
163
+ type RuntimeServices = EndpointServices | ThreadObjectNamespace;
164
+ type ThreadObjectInitializationError =
113
165
  | CloudflareDurableRuntimeInitializationError
114
166
  | CloudflareBindingError
115
167
  | MaintenancePassFailure;
116
168
 
117
- /** Port envelope tags whose owner-side execution durably mutates this Object's lane. */
118
- const MUTATING_PORT_TAGS: ReadonlySet<string> = new Set([
119
- "LedgerAdmit",
120
- "LedgerMarkReady",
121
- "LedgerRequestAbort",
122
- "LedgerRecordChildSettled",
123
- "StoreMaterialize",
124
- "StoreAppend",
125
- ]);
169
+ /** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
170
+ const isMutatingPortRequest = (request: PortRequest): boolean => {
171
+ switch (request._tag) {
172
+ case "LedgerAdmit":
173
+ case "LedgerMarkReady":
174
+ case "LedgerRequestAbort":
175
+ case "LedgerRecordChildSettled":
176
+ case "StoreMaterialize":
177
+ case "StoreAppend":
178
+ return true;
179
+ case "LedgerLookup":
180
+ case "LedgerResolveAdmission":
181
+ case "StoreReadPage":
182
+ case "StoreInspectTail":
183
+ case "StoreExport":
184
+ return false;
185
+ }
186
+ request satisfies never;
126
187
 
127
- const isMutatingPortRequest = (encoded: unknown): boolean =>
128
- typeof encoded === "object" &&
129
- encoded !== null &&
130
- "_tag" in encoded &&
131
- typeof encoded._tag === "string" &&
132
- MUTATING_PORT_TAGS.has(encoded._tag);
188
+ return false;
189
+ };
133
190
 
134
191
  /** The literal encoded `PortFailed(PortProtocolError)` fallback (same shape as WP2's). */
135
192
  const encodedPortProtocolFailure = (message: string): unknown => ({
@@ -165,7 +222,7 @@ const encodeResponse = (response: HostResponse): Effect.Effect<unknown> =>
165
222
  ),
166
223
  );
167
224
 
168
- const utf8Bytes = (value: unknown): number =>
225
+ const utf8Bytes = (value: PersistedJson): number =>
169
226
  new TextEncoder().encode(JSON.stringify(value)).length;
170
227
 
171
228
  /**
@@ -174,55 +231,57 @@ const utf8Bytes = (value: unknown): number =>
174
231
  * exempt: its accepted-work obligation already exists, and returning the original Receipt
175
232
  * consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
176
233
  */
177
- const gateAdmissionLimits = Effect.fn("ConversationObject.gateAdmissionLimits")(
178
- function* (request: {
179
- readonly principal: SubmissionLookupByKey["principal"];
180
- readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
181
- readonly inputPayload: PersistedJson;
182
- }) {
183
- const identity = yield* ConversationObjectIdentity;
184
- const config = yield* CloudflareDurableRuntimeConfig;
185
- const ledger = yield* SubmissionLedger;
186
- const { ctx } = yield* DurableObjectContext;
187
-
188
- const existing = yield* ledger.lookup(
189
- SubmissionLookupByKey.make({
190
- conversationId: identity.conversationId,
191
- principal: request.principal,
192
- idempotencyKey: request.idempotencyKey,
193
- }),
194
- );
195
- if (Option.isSome(existing)) return;
196
-
197
- const inputBytes = utf8Bytes(request.inputPayload);
198
- if (inputBytes > config.limits.maxInputBytes) {
199
- return yield* AdmissionLimitExceeded.make({
200
- limit: "input-bytes",
201
- actual: inputBytes,
202
- maximum: config.limits.maxInputBytes,
203
- });
204
- }
234
+ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request: {
235
+ readonly principal: SubmissionLookupByKey["principal"];
236
+ readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
237
+ readonly inputPayload: PersistedJson;
238
+ }) {
239
+ const identity = yield* ThreadObjectIdentity;
240
+ const config = yield* CloudflareDurableRuntimeConfig;
241
+ const ledger = yield* SubmissionLedger;
242
+ const { ctx } = yield* DurableObjectContext;
243
+
244
+ const existing = yield* ledger.lookup(
245
+ SubmissionLookupByKey.make({
246
+ threadId: identity.threadId,
247
+ principal: request.principal,
248
+ idempotencyKey: request.idempotencyKey,
249
+ }),
250
+ );
205
251
 
206
- // One Conversation per Object (durability §5): the local scan IS this lane's queue.
207
- const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
208
- if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
209
- return yield* AdmissionLimitExceeded.make({
210
- limit: "queue-depth",
211
- actual: nonterminal.length,
212
- maximum: config.limits.maxQueueDepthPerLane,
213
- });
214
- }
252
+ if (Option.isSome(existing)) return;
215
253
 
216
- const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
217
- if (databaseBytes > config.limits.maxDatabaseBytes) {
218
- return yield* AdmissionLimitExceeded.make({
219
- limit: "database-bytes",
220
- actual: databaseBytes,
221
- maximum: config.limits.maxDatabaseBytes,
222
- });
223
- }
224
- },
225
- );
254
+ const inputBytes = utf8Bytes(request.inputPayload);
255
+
256
+ if (inputBytes > config.limits.maxInputBytes) {
257
+ return yield* AdmissionLimitExceeded.make({
258
+ limit: "input-bytes",
259
+ actual: inputBytes,
260
+ maximum: config.limits.maxInputBytes,
261
+ });
262
+ }
263
+
264
+ // One Thread per Object (durability §5): the local scan IS this lane's queue.
265
+ const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
266
+
267
+ if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
268
+ return yield* AdmissionLimitExceeded.make({
269
+ limit: "queue-depth",
270
+ actual: nonterminal.length,
271
+ maximum: config.limits.maxQueueDepthPerLane,
272
+ });
273
+ }
274
+
275
+ const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
276
+
277
+ if (databaseBytes > config.limits.maxDatabaseBytes) {
278
+ return yield* AdmissionLimitExceeded.make({
279
+ limit: "database-bytes",
280
+ actual: databaseBytes,
281
+ maximum: config.limits.maxDatabaseBytes,
282
+ });
283
+ }
284
+ });
226
285
 
227
286
  /**
228
287
  * The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
@@ -242,22 +301,35 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
242
301
  Effect.mapError(protocolFailure("The submit request could not be decoded")),
243
302
  Effect.flatMap((request) =>
244
303
  Effect.gen(function* () {
245
- const identity = yield* ConversationObjectIdentity;
246
- const maintenance = yield* ConversationMaintenance;
304
+ const identity = yield* ThreadObjectIdentity;
305
+ const maintenance = yield* ThreadMaintenance;
247
306
  const runtime = yield* DurableAgentRuntime;
307
+
248
308
  yield* gateAdmissionLimits(request);
249
- // Alarm invariant: the alarm commits BEFORE the admission it will finish (D-P6-2).
250
- yield* maintenance.preArm;
251
- const receipt = yield* runtime.submit(
252
- passthroughSubmitAgent(request.agentId),
253
- request.inputPayload,
254
- {
255
- conversationId: identity.conversationId,
309
+
310
+ // Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
311
+ // cannot acknowledge that generation until this mutation leaves its public RPC seam.
312
+ const receipt = yield* maintenance.withMutation(
313
+ runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
314
+ threadId: identity.threadId,
256
315
  principal: request.principal,
257
316
  idempotencyKey: request.idempotencyKey,
317
+ ...(request.admissionGroup === undefined
318
+ ? {}
319
+ : { admissionGroup: request.admissionGroup }),
320
+ ...(request.admissionFence === undefined
321
+ ? {}
322
+ : { admissionFence: request.admissionFence }),
323
+ ...(request.workerAdmission === undefined
324
+ ? {}
325
+ : { workerAdmission: request.workerAdmission }),
326
+ ...(request.messageAdmission === undefined
327
+ ? {}
328
+ : { messageAdmission: request.messageAdmission }),
258
329
  definitions: request.definitions,
259
- },
330
+ }),
260
331
  );
332
+
261
333
  return SubmitSucceeded.make({ receipt });
262
334
  }),
263
335
  ),
@@ -265,6 +337,22 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
265
337
  Effect.flatMap(encodeResponse),
266
338
  );
267
339
 
340
+ const submissionStatusEndpoint = (
341
+ encoded: unknown,
342
+ ): Effect.Effect<unknown, never, EndpointServices> =>
343
+ decodeReceipt(encoded).pipe(
344
+ Effect.mapError(protocolFailure("The receipt could not be decoded")),
345
+ Effect.flatMap((receipt) =>
346
+ Effect.gen(function* () {
347
+ const runtime = yield* DurableAgentRuntime;
348
+
349
+ return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
350
+ }),
351
+ ),
352
+ respond,
353
+ Effect.flatMap(encodeResponse),
354
+ );
355
+
268
356
  const awaitSettlementEndpoint = (
269
357
  encoded: unknown,
270
358
  ): Effect.Effect<unknown, never, EndpointServices> =>
@@ -274,6 +362,7 @@ const awaitSettlementEndpoint = (
274
362
  Effect.gen(function* () {
275
363
  const runtime = yield* DurableAgentRuntime;
276
364
  const settlement = yield* runtime.awaitSettlement(receipt);
365
+
277
366
  return SettlementReached.make({ settlement });
278
367
  }),
279
368
  ),
@@ -281,28 +370,73 @@ const awaitSettlementEndpoint = (
281
370
  Effect.flatMap(encodeResponse),
282
371
  );
283
372
 
373
+ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
374
+ decodeAwaitProgressRequest(encoded).pipe(
375
+ Effect.mapError(protocolFailure("The progress request could not be decoded")),
376
+ Effect.flatMap((request) =>
377
+ Effect.gen(function* () {
378
+ const identity = yield* ThreadObjectIdentity;
379
+ const runtime = yield* DurableAgentRuntime;
380
+ const registry = yield* ProgressWaitRegistry;
381
+
382
+ yield* Effect.scoped(
383
+ Effect.gen(function* () {
384
+ const cancelled = yield* registry.subscribe(request.waiterId);
385
+
386
+ yield* Effect.raceFirst(
387
+ runtime.awaitProgress(identity.threadId, request.afterSequence),
388
+ cancelled,
389
+ );
390
+ }),
391
+ );
392
+
393
+ return ProgressObserved.make();
394
+ }),
395
+ ),
396
+ respond,
397
+ Effect.flatMap(encodeResponse),
398
+ );
399
+
400
+ const cancelProgressEndpoint = (
401
+ encoded: unknown,
402
+ ): Effect.Effect<unknown, never, EndpointServices> =>
403
+ decodeCancelProgressRequest(encoded).pipe(
404
+ Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
405
+ Effect.flatMap((request) =>
406
+ Effect.gen(function* () {
407
+ const registry = yield* ProgressWaitRegistry;
408
+
409
+ yield* registry.cancel(request.waiterId);
410
+
411
+ return ProgressCancelled.make();
412
+ }),
413
+ ),
414
+ respond,
415
+ Effect.flatMap(encodeResponse),
416
+ );
417
+
284
418
  const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
285
419
  decodeObservePageRequest(encoded).pipe(
286
420
  Effect.mapError(protocolFailure("The observe request could not be decoded")),
287
421
  Effect.flatMap((request) =>
288
422
  Effect.gen(function* () {
289
- const identity = yield* ConversationObjectIdentity;
290
- const store = yield* ConversationStore;
423
+ const identity = yield* ThreadObjectIdentity;
424
+ const store = yield* ThreadStore;
291
425
  // The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
292
426
  // the default reference preserves the possession behavior.
293
427
  const authorizer = yield* OperationAuthorizer;
294
- yield* authorizer
295
- .authorize(
296
- OperationAuthorizationRequest.make({
297
- operation: "observe",
298
- conversationId: identity.conversationId,
299
- }),
300
- )
301
- .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
428
+
429
+ yield* authorizer.authorize(
430
+ OperationAuthorizationRequest.make({
431
+ operation: "observe",
432
+ threadId: identity.threadId,
433
+ }),
434
+ );
435
+
302
436
  const records = yield* Stream.runCollect(
303
437
  store.read(
304
- ConversationRead.make({
305
- conversationId: identity.conversationId,
438
+ ThreadRead.make({
439
+ threadId: identity.threadId,
306
440
  ...(request.afterSequence === undefined
307
441
  ? {}
308
442
  : { afterSequence: request.afterSequence }),
@@ -310,6 +444,7 @@ const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
310
444
  }),
311
445
  ),
312
446
  );
447
+
313
448
  return ObservedPage.make({ records: [...records] });
314
449
  }),
315
450
  ),
@@ -322,10 +457,10 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
322
457
  Effect.mapError(protocolFailure("The abort command could not be decoded")),
323
458
  Effect.flatMap((command) =>
324
459
  Effect.gen(function* () {
325
- const maintenance = yield* ConversationMaintenance;
460
+ const maintenance = yield* ThreadMaintenance;
326
461
  const runtime = yield* DurableAgentRuntime;
327
- yield* maintenance.preArm;
328
- const intent = yield* runtime.abort(command);
462
+ const intent = yield* maintenance.withMutation(runtime.abort(command));
463
+
329
464
  return AbortRecorded.make({ intent });
330
465
  }),
331
466
  ),
@@ -333,24 +468,6 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
333
468
  Effect.flatMap(encodeResponse),
334
469
  );
335
470
 
336
- /**
337
- * The pre-P7 host protocol's failure union does not carry `OperationDenied` (the Worker client
338
- * predates the authorizer). This assembly always runs the default possession authorizer — no
339
- * `CloudflareDurableRuntimeOptions` authorizer lever exists yet — so a denial here is
340
- * unreachable today; if one ever surfaces it degrades to the protocol failure instead of an
341
- * out-of-contract throw. The four P7 admin entry points below carry `OperationDenied` typed.
342
- */
343
- const deniedToProtocolFailure = (
344
- denied: OperationDenied,
345
- ): Effect.Effect<never, HostProtocolError> =>
346
- Effect.fail(
347
- HostProtocolError.make({
348
- message: boundHostDiagnostic(
349
- `The ${denied.operation} operation was denied: ${denied.reason}`,
350
- ),
351
- }),
352
- );
353
-
354
471
  const resolveApprovalEndpoint = (
355
472
  encoded: unknown,
356
473
  ): Effect.Effect<unknown, never, EndpointServices> =>
@@ -358,12 +475,10 @@ const resolveApprovalEndpoint = (
358
475
  Effect.mapError(protocolFailure("The approval command could not be decoded")),
359
476
  Effect.flatMap((command) =>
360
477
  Effect.gen(function* () {
361
- const maintenance = yield* ConversationMaintenance;
478
+ const maintenance = yield* ThreadMaintenance;
362
479
  const runtime = yield* DurableAgentRuntime;
363
- yield* maintenance.preArm;
364
- const intent = yield* runtime
365
- .resolveApproval(command)
366
- .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
480
+ const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
481
+
367
482
  return ApprovalRecorded.make({ intent });
368
483
  }),
369
484
  ),
@@ -378,12 +493,10 @@ const resolveUnknownEndpoint = (
378
493
  Effect.mapError(protocolFailure("The resolution command could not be decoded")),
379
494
  Effect.flatMap((command) =>
380
495
  Effect.gen(function* () {
381
- const maintenance = yield* ConversationMaintenance;
496
+ const maintenance = yield* ThreadMaintenance;
382
497
  const runtime = yield* DurableAgentRuntime;
383
- yield* maintenance.preArm;
384
- const intent = yield* runtime
385
- .resolveUnknown(command)
386
- .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
498
+ const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
499
+
387
500
  return UnknownResolutionRecorded.make({ intent });
388
501
  }),
389
502
  ),
@@ -413,6 +526,7 @@ export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
413
526
 
414
527
  /** Every typed failure of the four admin entry points, plus the protocol's own errors. */
415
528
  export const AdminFailure = Schema.Union([
529
+ AdmissionPolicyError,
416
530
  OperationDenied,
417
531
  RetryRefused,
418
532
  LedgerError,
@@ -420,14 +534,15 @@ export const AdminFailure = Schema.Union([
420
534
  DigestError,
421
535
  OwnershipLost,
422
536
  SettlementConflict,
423
- ConversationStoreError,
424
- ConversationNotMaterialized,
537
+ ThreadStoreError,
538
+ ThreadNotMaterialized,
425
539
  AppendConflict,
426
540
  FenceRejected,
427
541
  DurableRuntimeFailpointError,
428
542
  DurableAlarmError,
429
543
  HostProtocolError,
430
544
  ]);
545
+
431
546
  export type AdminFailure = typeof AdminFailure.Type;
432
547
 
433
548
  export class ExplainedRecovery extends Schema.TaggedClass<ExplainedRecovery>(
@@ -469,6 +584,7 @@ export const AdminResponse = Schema.Union([
469
584
  ObligationsScanned,
470
585
  AdminFailed,
471
586
  ]);
587
+
472
588
  export type AdminResponse = typeof AdminResponse.Type;
473
589
 
474
590
  export const decodeAdminExplainRequest = Schema.decodeUnknownEffect(AdminExplainRequest);
@@ -506,12 +622,14 @@ const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoi
506
622
  Effect.mapError(protocolFailure("The explain request could not be decoded")),
507
623
  Effect.flatMap((request) =>
508
624
  Effect.gen(function* () {
509
- const identity = yield* ConversationObjectIdentity;
625
+ const identity = yield* ThreadObjectIdentity;
510
626
  const runtime = yield* DurableAgentRuntime;
627
+
511
628
  const explanations =
512
629
  request.submissionId === undefined
513
- ? yield* runtime.explainConversation(identity.conversationId)
630
+ ? yield* runtime.explainThread(identity.threadId)
514
631
  : [yield* runtime.explain(request.submissionId)];
632
+
515
633
  return ExplainedRecovery.make({ explanations });
516
634
  }),
517
635
  ),
@@ -524,9 +642,10 @@ const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
524
642
  Effect.mapError(protocolFailure("The verify request could not be decoded")),
525
643
  Effect.flatMap(() =>
526
644
  Effect.gen(function* () {
527
- const identity = yield* ConversationObjectIdentity;
645
+ const identity = yield* ThreadObjectIdentity;
528
646
  const runtime = yield* DurableAgentRuntime;
529
- const report = yield* runtime.verify(identity.conversationId);
647
+ const report = yield* runtime.verify(identity.threadId);
648
+
530
649
  return VerifiedIntegrity.make({ report });
531
650
  }),
532
651
  ),
@@ -539,12 +658,11 @@ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
539
658
  Effect.mapError(protocolFailure("The retry command could not be decoded")),
540
659
  Effect.flatMap((command) =>
541
660
  Effect.gen(function* () {
542
- const maintenance = yield* ConversationMaintenance;
661
+ const maintenance = yield* ThreadMaintenance;
543
662
  const runtime = yield* DurableAgentRuntime;
544
- // Alarm invariant: retry may repair durable state, so the alarm that will finish the
545
- // lane commits BEFORE the mutation (D-P6-2), exactly like abort.
546
- yield* maintenance.preArm;
547
- const report = yield* runtime.retry(command);
663
+ // Retry may repair durable state, so its generation + alarm commit before the mutation.
664
+ const report = yield* maintenance.withMutation(runtime.retry(command));
665
+
548
666
  return RetryExecuted.make({ report });
549
667
  }),
550
668
  ),
@@ -559,6 +677,7 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
559
677
  Effect.gen(function* () {
560
678
  const runtime = yield* DurableAgentRuntime;
561
679
  const report = yield* runtime.scanObligations(thresholds);
680
+
562
681
  return ObligationsScanned.make({ report });
563
682
  }),
564
683
  ),
@@ -567,62 +686,90 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
567
686
  );
568
687
 
569
688
  /**
570
- * Owner-side `portCall`: pre-arm before a mutating envelope (a routed admission committed by
571
- * THIS Object must already carry the alarm that will finish it), execute on the LOCAL facets
572
- * (never the routed decorators), then arm an immediate alarm so the mutated lane is
573
- * processed promptly. Protocol anomalies answer `PortFailed(PortProtocolError)`.
689
+ * Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
690
+ * public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
691
+ * finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
692
+ * alarm so the mutated lane is processed promptly. Protocol anomalies answer
693
+ * `PortFailed(PortProtocolError)`.
574
694
  */
575
695
  const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
576
696
  Effect.gen(function* () {
577
- const ports = yield* ConversationObjectPorts;
578
- const maintenance = yield* ConversationMaintenance;
697
+ const ports = yield* ThreadObjectPorts;
698
+ const maintenance = yield* ThreadMaintenance;
579
699
  const alarm = yield* DurableAlarmService;
580
- const mutating = isMutatingPortRequest(encoded);
581
- if (mutating) {
582
- const preArmed = yield* maintenance.preArm.pipe(Effect.exit);
583
- if (preArmed._tag === "Failure") {
584
- // Without the committed alarm the invariant cannot be promised; refuse the mutation.
585
- return encodedPortProtocolFailure(
586
- "The owner Object could not arm its maintenance alarm before the mutation.",
587
- );
588
- }
700
+
701
+ const decoded = yield* decodePortRequest(encoded).pipe(
702
+ Effect.map((request) => ({ _tag: "success" as const, request })),
703
+ Effect.catch((error) => Effect.succeed({ _tag: "failure" as const, message: error.message })),
704
+ );
705
+
706
+ if (decoded._tag === "failure") {
707
+ return encodedPortProtocolFailure(
708
+ `The port request could not be decoded: ${decoded.message}`,
709
+ );
710
+ }
711
+ const mutating = isMutatingPortRequest(decoded.request);
712
+
713
+ const handled = yield* (
714
+ mutating
715
+ ? maintenance.withMutation(ports.handle(decoded.request))
716
+ : ports.handle(decoded.request)
717
+ ).pipe(Effect.exit);
718
+
719
+ if (handled._tag === "Failure") {
720
+ // Without the committed generation/alarm the invariant cannot be promised; refuse before
721
+ // the port mutation runs. `ports.handle` itself is total, so this is the maintenance error.
722
+ return encodedPortProtocolFailure(
723
+ "The owner Object could not arm its maintenance alarm before the mutation.",
724
+ );
589
725
  }
590
- const response = yield* ports.handle(encoded);
726
+
727
+ const response = yield* encodePortResponse(handled.value).pipe(
728
+ Effect.catch((error) =>
729
+ Effect.succeed(
730
+ encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
731
+ ),
732
+ ),
733
+ );
734
+
591
735
  if (mutating) {
592
736
  // Prompt processing hint; the pre-armed alarm already guarantees convergence.
593
737
  yield* alarm.scheduleNow.pipe(
594
738
  Effect.catch((error) =>
595
- Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error),
739
+ Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error),
596
740
  ),
597
741
  );
598
742
  }
743
+
599
744
  return response;
600
745
  });
601
746
 
602
747
  const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
603
- const alarm = yield* DurableAlarmService;
604
- // Wake hints are droppable by contract: a failed alarm write is logged and swallowed; the
605
- // sender's own alarm/scan pairing (or this Object's next entry point) restores liveness.
606
- yield* alarm.scheduleNow.pipe(
607
- Effect.catch((error) => Effect.logWarning("ConversationObject.wake dropped", error)),
608
- );
748
+ const identity = yield* ThreadObjectIdentity;
749
+ const wake = yield* WakeScheduler;
750
+
751
+ // Route the remote hint through this incarnation's scheduler so scoped progress waiters and
752
+ // the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
753
+ yield* wake.notify(identity.threadId);
609
754
  });
610
755
 
611
756
  const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
612
757
  function* () {
613
- const maintenance = yield* ConversationMaintenance;
758
+ const maintenance = yield* ThreadMaintenance;
759
+
614
760
  // Typed pass failures propagate: the rejected promise makes workerd retry the alarm
615
- // (at-least-once delivery), and the pass's own pre-arm keeps the slot committed meanwhile.
761
+ // (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
616
762
  yield* maintenance.pass;
617
763
  },
618
764
  );
619
765
 
620
766
  const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
621
767
  function* () {
622
- // Forcing ConversationMaintenance forces the whole Layer stack: migration + exact-version
768
+ // Forcing ThreadMaintenance forces the whole Layer stack: migration + exact-version
623
769
  // check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
624
770
  // the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
625
- const maintenance = yield* ConversationMaintenance;
771
+ const maintenance = yield* ThreadMaintenance;
772
+
626
773
  yield* maintenance.ensureAlarm;
627
774
  },
628
775
  );
@@ -634,8 +781,9 @@ const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices
634
781
  */
635
782
  const effectCfPlatformLayer = (
636
783
  namespaceBinding: string,
784
+ rpcTracing = false,
637
785
  ): Layer.Layer<
638
- DurableObjectContext | ConversationObjectNamespace,
786
+ DurableObjectContext | ThreadObjectNamespace,
639
787
  CloudflareBindingError,
640
788
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
641
789
  > => {
@@ -643,27 +791,39 @@ const effectCfPlatformLayer = (
643
791
  Effect.gen(function* () {
644
792
  const state = yield* EffectCfDurableObjectState.DurableObjectState;
645
793
  const env = yield* WorkerEnvironment;
794
+
646
795
  return DurableObjectContext.of({ ctx: state.raw, env });
647
796
  }),
648
797
  );
649
- const namespace = Layer.effect(ConversationObjectNamespace)(
798
+
799
+ const namespace = Layer.effect(ThreadObjectNamespace)(
650
800
  Effect.gen(function* () {
651
801
  const env = yield* WorkerEnvironment;
652
- const binding = yield* conversationNamespaceFromEnv(env, namespaceBinding);
653
- return ConversationObjectNamespace.of({ namespace: binding });
802
+ const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
803
+
804
+ return ThreadObjectNamespace.of({
805
+ namespace: binding,
806
+ ...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
807
+ });
654
808
  }),
655
809
  );
810
+
656
811
  return Layer.merge(context, namespace);
657
812
  };
658
813
 
659
- /** The public endpoint surface of one Conversation Object instance. */
660
- export interface ConversationObjectInstance extends CloudflareDurableObject {
661
- submitEncoded(encoded: unknown): Promise<unknown>;
662
- awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
663
- observePage(encoded: unknown): Promise<unknown>;
664
- abortEncoded(encoded: unknown): Promise<unknown>;
665
- resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
666
- resolveUnknownEncoded(encoded: unknown): Promise<unknown>;
814
+ /** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
815
+ export interface Instance<EventServices = never> extends InstanceType<
816
+ EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
817
+ > {
818
+ submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
819
+ submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
820
+ awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
821
+ awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
822
+ cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
823
+ observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;
824
+ abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
825
+ resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
826
+ resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
667
827
  explainEncoded(encoded: unknown): Promise<unknown>;
668
828
  verifyEncoded(encoded: unknown): Promise<unknown>;
669
829
  retryEncoded(encoded: unknown): Promise<unknown>;
@@ -673,52 +833,60 @@ export interface ConversationObjectInstance extends CloudflareDurableObject {
673
833
  alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
674
834
  }
675
835
 
676
- /** The constructor shape workerd instantiates for each Conversation Object. */
677
- export interface ConversationObjectClass {
678
- new (ctx: DurableObjectState, env: Cloudflare.Env): ConversationObjectInstance;
836
+ /** The constructor shape workerd instantiates for each Thread Object. */
837
+ export interface Class<EventServices = never> {
838
+ new (ctx: DurableObjectState, env: Cloudflare.Env): Instance<EventServices>;
679
839
  }
680
840
 
681
841
  /**
682
- * Build the application's Conversation Object class (export it from the Worker entry).
683
- * effect-cf owns the cached ManagedRuntime, native RPC methods, event scopes, and post-handler
684
- * OTLP flush scheduling for RPC and alarm events. The optional outer Layer is built per native
685
- * event, so a host can install Tracer/Logger/Metric services and `OtlpExporter.Flusher` without
686
- * Effect Agent owning exporter lifecycle machinery.
842
+ * Export a composed application Layer as a native Durable Object class.
843
+ * Bootstrap services are provided to the whole graph before it acquires, so application Layers
844
+ * can yield effect-cf's WorkerEnvironment and DurableObjectState, derived identity, and Crypto.
845
+ * Application dependencies remain visible until Layer.provide satisfies them. effect-cf owns the
846
+ * cached ManagedRuntime, native RPC methods, event scopes, and telemetry flushing.
847
+ * Initialization is local and bounded inside the constructor gate. Cloudflare eviction does not
848
+ * guarantee finalizers; put resources requiring timely release in scoped operations or eventLayer.
687
849
  */
688
- export const makeConversationObjectClass = <EventLayerError = never, EventServices = never>(
689
- options: ConversationObjectOptions,
690
- observability?: Layer.Layer<
691
- EventServices,
692
- EventLayerError,
693
- | DurableObjectContext
694
- | ConversationObjectNamespace
850
+ export const make = <
851
+ ApplicationServices,
852
+ ApplicationError,
853
+ EventServices = never,
854
+ EventLayerError = never,
855
+ >(
856
+ applicationLayer: Layer.Layer<
857
+ CloudflareDurableRuntimeServices | ApplicationServices,
858
+ ApplicationError,
859
+ | CloudflareBootstrapServices
695
860
  | EffectCfDurableObjectState.DurableObjectState
696
861
  | WorkerEnvironment
862
+ | DurableObjectContext
863
+ | ThreadObjectNamespace
697
864
  >,
698
- ): ConversationObjectClass => {
699
- const application: Layer.Layer<
700
- RuntimeServices,
701
- CloudflareDurableRuntimeInitializationError | CloudflareBindingError,
702
- EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
703
- > = CloudflareDurableRuntime.layer(options).pipe(
704
- Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding)),
865
+ options: Options<ApplicationServices, EventServices, EventLayerError>,
866
+ ): Class<ApplicationServices | EventServices> => {
867
+ const application = applicationLayer.pipe(
868
+ Layer.provideMerge(layerConfig(options)),
869
+ Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
705
870
  );
706
871
 
707
872
  // The storage/config Layer must acquire inside Cloudflare's constructor gate. effect-cf owns
708
873
  // the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
709
874
  // before migration, compatibility checks, or alarm inspection touch Object storage.
710
875
  const runtime: Layer.Layer<
711
- RuntimeServices,
712
- ConversationObjectInitializationError,
876
+ RuntimeServices | ApplicationServices,
877
+ ThreadObjectInitializationError | ApplicationError,
713
878
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
714
879
  > = Layer.effectContext(
715
880
  Effect.gen(function* () {
716
881
  const state = yield* EffectCfDurableObjectState.DurableObjectState;
717
882
  const scope = yield* Effect.scope;
883
+
718
884
  return yield* state.blockConcurrencyWhile(
719
885
  Effect.gen(function* () {
720
886
  const services = yield* Layer.buildWithScope(application, scope);
887
+
721
888
  yield* gateEndpoint.pipe(Effect.provide(services));
889
+
722
890
  return services;
723
891
  }),
724
892
  );
@@ -727,7 +895,10 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
727
895
 
728
896
  const rpc = {
729
897
  submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
898
+ submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
730
899
  awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
900
+ awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
901
+ cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),
731
902
  observePage: (encoded: unknown) => observePageEndpoint(encoded),
732
903
  abortEncoded: (encoded: unknown) => abortEndpoint(encoded),
733
904
  resolveApprovalEncoded: (encoded: unknown) => resolveApprovalEndpoint(encoded),
@@ -738,31 +909,44 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
738
909
  obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
739
910
  portCall: (encoded: unknown) => portCallEndpoint(encoded),
740
911
  wake: () => wakeEndpoint,
741
- } satisfies EffectCfDurableObject.DurableObjectRpc<RuntimeServices | EventServices>;
912
+ } satisfies EffectCfDurableObject.DurableObjectRpc<
913
+ RuntimeServices | ApplicationServices | EventServices
914
+ >;
742
915
 
743
- const EffectCfConversationObject = EffectCfDurableObject.make<
744
- RuntimeServices,
745
- ConversationObjectInitializationError,
916
+ type NativeOptions = EffectCfDurableObject.DurableObjectOptions<
917
+ RuntimeServices | ApplicationServices,
918
+ EventServices,
919
+ EventLayerError,
920
+ typeof rpc
921
+ >;
922
+
923
+ const EffectCfThreadObject = EffectCfDurableObject.make<
924
+ RuntimeServices | ApplicationServices,
925
+ ThreadObjectInitializationError | ApplicationError,
746
926
  EventServices,
747
927
  EventLayerError,
748
928
  typeof rpc
749
929
  >(runtime, {
750
- ...(observability === undefined ? {} : { eventLayer: observability }),
930
+ ...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
931
+ ...(options.eventLayer === undefined ? {} : { eventLayer: options.eventLayer }),
751
932
  // Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
752
933
  // in each bounded pass so cross-Object initialization cannot deadlock.
753
934
  initialize: Effect.void,
754
935
  rpc,
755
936
  alarm: () => alarmEndpoint,
756
- });
937
+ // This host owns the raw alarm and supplies event services through options.eventLayer.
938
+ // Upstream's conditional alarm-registration check cannot reduce over generic application
939
+ // services. Options and the rpc satisfies check above retain their Effect requirements.
940
+ } as NativeOptions);
757
941
 
758
942
  // effect-cf's class type keeps `alarm` optional even when the handler option is present. This
759
943
  // concrete override reflects this factory's stronger contract while delegating execution to
760
944
  // the effect-cf runtime unchanged.
761
- class ConversationObject extends EffectCfConversationObject {
945
+ class ThreadObject extends EffectCfThreadObject {
762
946
  override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
763
947
  return super.alarm?.(alarmInfo);
764
948
  }
765
949
  }
766
950
 
767
- return ConversationObject;
951
+ return ThreadObject;
768
952
  };