@effect-agent/platform-cloudflare 0.1.0-beta.11 → 0.1.0-beta.110

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/dist/Alarm.d.mts +208 -0
  2. package/dist/Alarm.mjs +601 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserRestCapture.d.mts +34 -0
  5. package/dist/BrowserRestCapture.mjs +238 -0
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/BrowserRestCrawl.d.mts +16 -0
  8. package/dist/BrowserRestCrawl.mjs +244 -0
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/CloudflareAiGateway.d.mts +64 -0
  11. package/dist/CloudflareAiGateway.mjs +70 -0
  12. package/dist/CloudflareAiGateway.mjs.map +1 -0
  13. package/dist/CloudflareBindings.d.mts +115 -0
  14. package/dist/CloudflareBindings.mjs +113 -0
  15. package/dist/CloudflareBindings.mjs.map +1 -0
  16. package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
  17. package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
  18. package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
  19. package/dist/CloudflareBrowser.d.mts +2 -0
  20. package/dist/CloudflareBrowser.mjs +2 -0
  21. package/dist/CloudflareCodeMode.d.mts +44 -0
  22. package/dist/CloudflareCodeMode.mjs +616 -0
  23. package/dist/CloudflareCodeMode.mjs.map +1 -0
  24. package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
  25. package/dist/CloudflareConfig.d.mts +2 -0
  26. package/dist/CloudflareConfig.mjs +128 -0
  27. package/dist/CloudflareConfig.mjs.map +1 -0
  28. package/dist/CloudflareMemory.d.mts +142 -0
  29. package/dist/CloudflareMemory.mjs +202 -0
  30. package/dist/CloudflareMemory.mjs.map +1 -0
  31. package/dist/CloudflareScheduling.d.mts +52 -0
  32. package/dist/CloudflareScheduling.mjs +396 -0
  33. package/dist/CloudflareScheduling.mjs.map +1 -0
  34. package/dist/CloudflareSubscriptions.d.mts +87 -0
  35. package/dist/CloudflareSubscriptions.mjs +530 -0
  36. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  37. package/dist/CloudflareThreadClient.d.mts +1675 -0
  38. package/dist/CloudflareThreadClient.mjs +382 -0
  39. package/dist/CloudflareThreadClient.mjs.map +1 -0
  40. package/dist/InteractiveBrowser-CKp6pqmw.d.mts +171 -0
  41. package/dist/InteractiveBrowser-l6J4eP6T.mjs +1427 -0
  42. package/dist/InteractiveBrowser-l6J4eP6T.mjs.map +1 -0
  43. package/dist/InteractiveBrowser.d.mts +2 -0
  44. package/dist/InteractiveBrowser.mjs +2 -0
  45. package/dist/ProtectedBrowser.d.mts +123 -0
  46. package/dist/ProtectedBrowser.mjs +1161 -0
  47. package/dist/ProtectedBrowser.mjs.map +1 -0
  48. package/dist/ThreadObject-BGla_Obk.mjs +858 -0
  49. package/dist/ThreadObject-BGla_Obk.mjs.map +1 -0
  50. package/dist/ThreadObject-MSlpVBUD.d.mts +829 -0
  51. package/dist/ThreadObject.d.mts +2 -0
  52. package/dist/ThreadObject.mjs +3 -0
  53. package/dist/WakeScheduler.d.mts +23 -0
  54. package/dist/WakeScheduler.mjs +57 -0
  55. package/dist/WakeScheduler.mjs.map +1 -0
  56. package/dist/boundary-BguazVkh.mjs +42 -0
  57. package/dist/boundary-BguazVkh.mjs.map +1 -0
  58. package/dist/index.d.mts +13 -1548
  59. package/dist/index.mjs +13 -1636
  60. package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
  61. package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
  62. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  63. package/package.json +1 -53
  64. package/src/Alarm.ts +1323 -0
  65. package/src/BrowserRestCapture.ts +477 -0
  66. package/src/BrowserRestCrawl.ts +540 -0
  67. package/src/CloudflareAiGateway.ts +170 -0
  68. package/src/CloudflareBindings.ts +199 -0
  69. package/src/CloudflareBrowser.ts +15 -0
  70. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
  71. package/src/{config.ts → CloudflareConfig.ts} +19 -12
  72. package/src/CloudflareMemory.ts +420 -0
  73. package/src/CloudflareScheduling.ts +747 -0
  74. package/src/CloudflareSubscriptions.ts +1041 -0
  75. package/src/CloudflareThreadClient.ts +809 -0
  76. package/src/InteractiveBrowser.ts +2612 -0
  77. package/src/ProtectedBrowser.ts +9 -0
  78. package/src/ThreadObject.ts +1184 -0
  79. package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
  80. package/src/index.ts +12 -23
  81. package/src/internal/boundary.ts +55 -0
  82. package/src/internal/browser-binding.ts +82 -0
  83. package/src/internal/browser-failure.ts +92 -0
  84. package/src/internal/browser-file-selection.ts +126 -0
  85. package/src/internal/browser-quick-action.ts +857 -0
  86. package/src/internal/browser-session-lifecycle.ts +193 -0
  87. package/src/internal/layers.ts +765 -0
  88. package/src/internal/message-delivery.ts +181 -0
  89. package/src/internal/prepared-admission.ts +116 -0
  90. package/src/internal/progress-wait.ts +121 -0
  91. package/src/internal/transport.ts +44 -0
  92. package/src/protected-browser/binding.ts +354 -0
  93. package/src/protected-browser/host.ts +379 -0
  94. package/src/protected-browser/inspect-frame.ts +125 -0
  95. package/src/protected-browser/native.ts +473 -0
  96. package/src/protected-browser/policy.ts +857 -0
  97. package/dist/index.mjs.map +0 -1
  98. package/src/alarm.ts +0 -334
  99. package/src/bindings.ts +0 -145
  100. package/src/client.ts +0 -646
  101. package/src/conversation-object.ts +0 -768
  102. package/src/layers.ts +0 -376
  103. package/src/transport.ts +0 -38
@@ -0,0 +1,1184 @@
1
+ import {
2
+ decodePortRequest,
3
+ encodePortResponse,
4
+ LedgerLookupResult,
5
+ PortFailed,
6
+ PortProtocolError,
7
+ PortSucceeded,
8
+ type PortRequest,
9
+ type PortResponse,
10
+ } from "@effect-agent/storage-cloudflare/port-protocol";
11
+ import { Effect, Layer, Option, Schema, Stream } from "effect";
12
+ import {
13
+ IntegrityReport,
14
+ ObligationReport,
15
+ ObligationThresholds,
16
+ RecoveryExplanation,
17
+ RetryCommand,
18
+ RetryRefused,
19
+ } from "effect-agent/admin";
20
+ import { DigestError } from "effect-agent/digest";
21
+ import {
22
+ DurableAgentRuntime,
23
+ DurableRuntimeConfig,
24
+ RecoveryReport,
25
+ type DurableSubmitAgent,
26
+ } from "effect-agent/durable-agent-runtime";
27
+ import { DurableRuntimeFailpointError } from "effect-agent/durable-failpoint";
28
+ import { type AgentId, type ThreadId } from "effect-agent/identifiers";
29
+ import { SubmissionId } from "effect-agent/identifiers";
30
+ import {
31
+ OperationAuthorizationRequest,
32
+ OperationAuthorizer,
33
+ OperationDenied,
34
+ } from "effect-agent/operation-authorizer";
35
+ import { PersistedJson } from "effect-agent/records";
36
+ import { RunJournalError } from "effect-agent/run-journal";
37
+ import {
38
+ AdmissionPolicyError,
39
+ LedgerError,
40
+ OwnershipLost,
41
+ SettlementConflict,
42
+ SubmissionLedger,
43
+ SubmissionLookupByKey,
44
+ } from "effect-agent/submission-ledger";
45
+ import {
46
+ AppendConflict,
47
+ ThreadNotMaterialized,
48
+ ThreadRead,
49
+ ThreadStore,
50
+ ThreadStoreError,
51
+ FenceRejected,
52
+ } from "effect-agent/thread-store";
53
+ import { WakeScheduler } from "effect-agent/wake-scheduler";
54
+ import {
55
+ DurableObject as EffectCfDurableObject,
56
+ DurableObjectState as EffectCfDurableObjectState,
57
+ WorkerEnvironment,
58
+ } from "effect-cf";
59
+
60
+ import {
61
+ ThreadMaintenance,
62
+ DurableAlarmError,
63
+ DurableAlarmService,
64
+ ThreadMutationGate,
65
+ publishCommitted,
66
+ type MaintenancePassFailure,
67
+ } from "./Alarm.ts";
68
+ import {
69
+ ThreadObjectIdentity,
70
+ ThreadObjectPlacement,
71
+ DurableObjectContext,
72
+ ThreadObjectNamespace,
73
+ threadNamespaceFromEnv,
74
+ type CloudflareBindingError,
75
+ } from "./CloudflareBindings.ts";
76
+ import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.ts";
77
+ import {
78
+ AbortRecorded,
79
+ ApprovalRecorded,
80
+ HostFailed,
81
+ HostProtocolError,
82
+ ObservedPage,
83
+ ProgressObserved,
84
+ ProgressCancelled,
85
+ SettlementReached,
86
+ SubmissionStatusResponse,
87
+ SubmitSucceeded,
88
+ UnknownResolutionRecorded,
89
+ boundHostDiagnostic,
90
+ decodeAbortCommand,
91
+ decodeAwaitProgressRequest,
92
+ decodeCancelProgressRequest,
93
+ decodeApprovalDecisionCommand,
94
+ decodeObservePageRequest,
95
+ decodeReceipt,
96
+ decodeSubmitRequest,
97
+ decodeUnknownResolutionCommand,
98
+ encodeHostResponse,
99
+ type HostFailure,
100
+ type HostResponse,
101
+ type SubmitRequest,
102
+ } from "./CloudflareThreadClient.ts";
103
+ import {
104
+ layerConfig,
105
+ ThreadObjectPorts,
106
+ type CloudflareDurableRuntimeInitializationError,
107
+ type CloudflareDurableRuntimeOptions,
108
+ type CloudflareDurableRuntimeServices,
109
+ type CloudflareBootstrapServices,
110
+ } from "./internal/layers.ts";
111
+ import { ProgressWaitRegistry } from "./internal/progress-wait.ts";
112
+
113
+ export {
114
+ layer,
115
+ layerConfig,
116
+ layerHostConfig,
117
+ layerInHost,
118
+ ThreadObjectPorts,
119
+ type ThreadPublicationOptions as PublicationOptions,
120
+ type CloudflareDurableRuntimeOptions as RuntimeOptions,
121
+ type CloudflareDurableRuntimeServices as Services,
122
+ type CloudflareDurableRuntimeInitializationError as InitializationError,
123
+ type CloudflareBootstrapServices as BootstrapServices,
124
+ } from "./internal/layers.ts";
125
+
126
+ /**
127
+ * `ThreadObject.make(application, options)` — the Thread Durable Object
128
+ * (plan §1.4,
129
+ * D-P6-1): a factory returning a class that applications export from their Worker entry.
130
+ * One SQLite-backed Object per Thread is the serialized owner (durability §6); the
131
+ * Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
132
+ * ONE bounded `runRecovery` + `processThreadHead` pass, and the persisted alarm
133
+ * (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
134
+ * incoming request.
135
+ * `Services` exposes the same owner `SqlClient` used by the Thread stores. Compose optional
136
+ * local repositories after `ThreadObject.layer`; never acquire another independently locked
137
+ * SQL client for the same Object. Exposing the client installs no additional storage schemas.
138
+ *
139
+ * Constructor gate (`blockConcurrencyWhile`) is LOCAL-ONLY: schema migration and the
140
+ * exact-version check, configuration decode, and the defensive ensure-alarm half of the
141
+ * alarm invariant. It deliberately does NOT run the recovery pass: parent recovery can
142
+ * require child-Object reads and vice versa, and two Objects blocked in constructor gates
143
+ * awaiting each other's RPC would deadlock (plan §1.4). Instead every pass runs
144
+ * `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
145
+ */
146
+
147
+ /** Construction options for one deployed Thread Object class. */
148
+ export interface Options<
149
+ ApplicationServices = never,
150
+ EventServices = never,
151
+ EventLayerError = never,
152
+ > extends CloudflareDurableRuntimeOptions {
153
+ /** Accept transient native RPC tracing through effect-cf; disabled by default. */
154
+ readonly rpcTracing?: boolean;
155
+ /**
156
+ * Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
157
+ * Object's route back to sibling Thread Objects for the WP2 cross-Object port calls
158
+ * and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
159
+ */
160
+ readonly namespaceBinding: string;
161
+ /** Acquired and finalized per native event, with access to the complete application runtime. */
162
+ readonly eventLayer?: Layer.Layer<
163
+ EventServices,
164
+ EventLayerError,
165
+ | RuntimeServices
166
+ | ApplicationServices
167
+ | EffectCfDurableObjectState.DurableObjectState
168
+ | WorkerEnvironment
169
+ >;
170
+ }
171
+
172
+ type EndpointServices =
173
+ | CloudflareDurableRuntimeServices
174
+ | CloudflareBootstrapServices
175
+ | DurableObjectContext;
176
+ type RuntimeServices = EndpointServices | ThreadObjectNamespace;
177
+ type ThreadObjectInitializationError =
178
+ | CloudflareDurableRuntimeInitializationError
179
+ | CloudflareBindingError
180
+ | MaintenancePassFailure;
181
+
182
+ /** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
183
+ const isMutatingPortRequest = (request: PortRequest): boolean => {
184
+ switch (request._tag) {
185
+ case "LedgerAdmit":
186
+ case "LedgerMarkReady":
187
+ case "LedgerRequestAbort":
188
+ case "LedgerRecordChildSettled":
189
+ case "StoreMaterialize":
190
+ case "StoreAppend":
191
+ return true;
192
+ case "LedgerLookup":
193
+ case "LedgerResolveAdmission":
194
+ case "StoreReadPage":
195
+ case "StoreInspectTail":
196
+ case "StoreCountPeerMessages":
197
+ case "StoreExport":
198
+ case "MessageDeliveryList":
199
+ return false;
200
+ }
201
+ request satisfies never;
202
+
203
+ return false;
204
+ };
205
+
206
+ /** The literal encoded `PortFailed(PortProtocolError)` fallback (same shape as WP2's). */
207
+ const encodedPortProtocolFailure = (message: string): unknown => ({
208
+ _tag: "PortFailed",
209
+ failure: { _tag: "PortProtocolError", message: boundHostDiagnostic(message) },
210
+ });
211
+
212
+ const protocolFailure = (context: string) => (error: { readonly message: string }) =>
213
+ HostProtocolError.make({
214
+ message: boundHostDiagnostic(`${context}: ${error.message}`),
215
+ });
216
+
217
+ /** Fold one endpoint's typed failures into the uniform `HostResponse` envelope. */
218
+ const respond = <Result extends HostResponse, Failure extends HostFailure>(
219
+ effect: Effect.Effect<Result, Failure, EndpointServices>,
220
+ ): Effect.Effect<HostResponse, never, EndpointServices> =>
221
+ effect.pipe(
222
+ Effect.map((result): HostResponse => result),
223
+ Effect.catch((failure) => Effect.succeed<HostResponse>(HostFailed.make({ failure }))),
224
+ );
225
+
226
+ /** Encode the response envelope; an unencodable response degrades to a protocol failure. */
227
+ const encodeResponse = (response: HostResponse): Effect.Effect<unknown> =>
228
+ encodeHostResponse(response).pipe(
229
+ Effect.catch((error) =>
230
+ Effect.succeed<unknown>({
231
+ _tag: "HostFailed",
232
+ failure: {
233
+ _tag: "HostProtocolError",
234
+ message: boundHostDiagnostic(`The host response could not be encoded: ${error.message}`),
235
+ },
236
+ }),
237
+ ),
238
+ );
239
+
240
+ const utf8Bytes = (value: PersistedJson): number =>
241
+ new TextEncoder().encode(JSON.stringify(value)).length;
242
+
243
+ /**
244
+ * The admission-limits gate, BEFORE `runtime.submit` touches the ledger (exit gate
245
+ * "resource limits are checked before admission"; DEPLOY-007). A replayed idempotency key is
246
+ * exempt: its accepted-work obligation already exists, and returning the original Receipt
247
+ * consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
248
+ */
249
+ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (
250
+ threadId: ThreadId,
251
+ request: {
252
+ readonly principal: SubmissionLookupByKey["principal"];
253
+ readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
254
+ readonly inputPayload: PersistedJson;
255
+ },
256
+ ) {
257
+ const config = yield* CloudflareDurableRuntimeConfig;
258
+ const ledger = yield* SubmissionLedger;
259
+ const { ctx } = yield* DurableObjectContext;
260
+
261
+ const existing = yield* ledger.lookup(
262
+ SubmissionLookupByKey.make({
263
+ threadId,
264
+ principal: request.principal,
265
+ idempotencyKey: request.idempotencyKey,
266
+ }),
267
+ );
268
+
269
+ if (Option.isSome(existing)) return;
270
+
271
+ const inputBytes = utf8Bytes(request.inputPayload);
272
+
273
+ if (inputBytes > config.limits.maxInputBytes) {
274
+ return yield* AdmissionLimitExceeded.make({
275
+ limit: "input-bytes",
276
+ actual: inputBytes,
277
+ maximum: config.limits.maxInputBytes,
278
+ });
279
+ }
280
+
281
+ const nonterminal = yield* ledger.scanNonterminal.pipe(
282
+ Stream.filter((submission) => submission.threadId === threadId),
283
+ Stream.runCollect,
284
+ );
285
+
286
+ if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
287
+ return yield* AdmissionLimitExceeded.make({
288
+ limit: "queue-depth",
289
+ actual: nonterminal.length,
290
+ maximum: config.limits.maxQueueDepthPerLane,
291
+ });
292
+ }
293
+
294
+ const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
295
+
296
+ if (databaseBytes > config.limits.maxDatabaseBytes) {
297
+ return yield* AdmissionLimitExceeded.make({
298
+ limit: "database-bytes",
299
+ actual: databaseBytes,
300
+ maximum: config.limits.maxDatabaseBytes,
301
+ });
302
+ }
303
+ });
304
+
305
+ /**
306
+ * The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
307
+ * already encoded through the real input schema on the Worker side (`client.ts`), so the
308
+ * Object admits the canonical `PersistedJson` payload as-is; the resolved Binding re-derives
309
+ * everything else from the stored `(agentId, agentDigests)` at claim time (SUB-023).
310
+ */
311
+ const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
312
+ definition: {
313
+ id: agentId,
314
+ input: PersistedJson,
315
+ },
316
+ });
317
+
318
+ /** A physical owner may hold other Threads; an addressed request cannot act on their IDs. */
319
+ const lookupAddressedSubmission = Effect.fn("ThreadObject.lookupAddressedSubmission")(function* (
320
+ submissionId: SubmissionId,
321
+ ) {
322
+ const { threadId } = yield* ThreadObjectIdentity;
323
+ const ports = yield* ThreadObjectPorts;
324
+ const submission = yield* ports.lookupSubmission(submissionId);
325
+
326
+ if (Option.isSome(submission) && submission.value.threadId !== threadId)
327
+ return yield* HostProtocolError.make({ message: "The Submission belongs to another Thread" });
328
+
329
+ return submission;
330
+ });
331
+
332
+ const requireSubmissionThread = Effect.fn("ThreadObject.requireSubmissionThread")(function* (
333
+ submissionId: SubmissionId,
334
+ ) {
335
+ const submission = yield* lookupAddressedSubmission(submissionId);
336
+
337
+ if (Option.isNone(submission))
338
+ return yield* LedgerError.make({
339
+ operation: "addressed Submission lookup",
340
+ message: "The addressed Thread has no such Submission",
341
+ });
342
+ });
343
+
344
+ const requireReceiptThread = Effect.fn("ThreadObject.requireReceiptThread")(function* (
345
+ threadId: ThreadId,
346
+ ) {
347
+ const identity = yield* ThreadObjectIdentity;
348
+
349
+ if (threadId !== identity.threadId)
350
+ return yield* HostProtocolError.make({ message: "The Receipt belongs to another Thread" });
351
+ });
352
+
353
+ const requirePortThread = (request: PortRequest) => {
354
+ switch (request._tag) {
355
+ case "MessageDeliveryList":
356
+ return requireReceiptThread(request.request.ownerThreadId);
357
+ case "LedgerLookup":
358
+ return request.request._tag === "SubmissionLookupById"
359
+ ? lookupAddressedSubmission(request.request.submissionId).pipe(Effect.asVoid)
360
+ : requireReceiptThread(request.request.threadId);
361
+ case "LedgerMarkReady":
362
+ case "LedgerRequestAbort":
363
+ return requireSubmissionThread(request.request.submissionId);
364
+ case "LedgerRecordChildSettled":
365
+ return requireSubmissionThread(request.request.parentSubmissionId);
366
+ case "LedgerAdmit":
367
+ case "LedgerResolveAdmission":
368
+ case "StoreMaterialize":
369
+ case "StoreAppend":
370
+ case "StoreReadPage":
371
+ case "StoreInspectTail":
372
+ case "StoreCountPeerMessages":
373
+ case "StoreExport":
374
+ return requireReceiptThread(request.request.threadId);
375
+ }
376
+ request satisfies never;
377
+ };
378
+
379
+ /**
380
+ * Admit an already Schema-decoded request to a logical Thread in this physical owner.
381
+ * Custom hosts validate local placement before calling this Effect and provide their same
382
+ * runtime/maintenance instances. The native endpoint uses this path too: queue limits,
383
+ * idempotent receipts and the pre-admission generation/alarm commit have one owner.
384
+ */
385
+ export const submit = Effect.fn("ThreadObject.submit")(function* (
386
+ threadId: ThreadId,
387
+ request: SubmitRequest,
388
+ ) {
389
+ const placement = yield* ThreadObjectPlacement;
390
+
391
+ if (!placement.ownsThread(threadId))
392
+ return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
393
+ const mutations = yield* ThreadMutationGate;
394
+ const runtime = yield* DurableAgentRuntime;
395
+
396
+ yield* gateAdmissionLimits(threadId, request);
397
+
398
+ return yield* mutations.withMutation(
399
+ runtime
400
+ .submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
401
+ threadId,
402
+ principal: request.principal,
403
+ idempotencyKey: request.idempotencyKey,
404
+ ...(request.admissionGroup === undefined ? {} : { admissionGroup: request.admissionGroup }),
405
+ ...(request.admissionFence === undefined ? {} : { admissionFence: request.admissionFence }),
406
+ ...(request.workerAdmission === undefined
407
+ ? {}
408
+ : { workerAdmission: request.workerAdmission }),
409
+ ...(request.messageAdmission === undefined
410
+ ? {}
411
+ : { messageAdmission: request.messageAdmission }),
412
+ definitions: request.definitions,
413
+ })
414
+ .pipe(Effect.tap(() => publishCommitted)),
415
+ );
416
+ });
417
+
418
+ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
419
+ decodeSubmitRequest(encoded).pipe(
420
+ Effect.mapError(protocolFailure("The submit request could not be decoded")),
421
+ Effect.flatMap((request) =>
422
+ Effect.gen(function* () {
423
+ const identity = yield* ThreadObjectIdentity;
424
+ const receipt = yield* submit(identity.threadId, request);
425
+
426
+ return SubmitSucceeded.make({ receipt });
427
+ }),
428
+ ),
429
+ respond,
430
+ Effect.flatMap(encodeResponse),
431
+ );
432
+
433
+ const submissionStatusEndpoint = (
434
+ encoded: unknown,
435
+ ): Effect.Effect<unknown, never, EndpointServices> =>
436
+ decodeReceipt(encoded).pipe(
437
+ Effect.mapError(protocolFailure("The receipt could not be decoded")),
438
+ Effect.flatMap((receipt) =>
439
+ Effect.gen(function* () {
440
+ const authorizer = yield* OperationAuthorizer;
441
+
442
+ yield* authorizer.authorize(
443
+ OperationAuthorizationRequest.make({
444
+ operation: "awaitSettlement",
445
+ threadId: receipt.threadId,
446
+ submissionId: receipt.submissionId,
447
+ }),
448
+ );
449
+ yield* requireReceiptThread(receipt.threadId);
450
+ yield* requireSubmissionThread(receipt.submissionId);
451
+ const runtime = yield* DurableAgentRuntime;
452
+
453
+ return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
454
+ }),
455
+ ),
456
+ respond,
457
+ Effect.flatMap(encodeResponse),
458
+ );
459
+
460
+ const awaitSettlementEndpoint = (
461
+ encoded: unknown,
462
+ ): Effect.Effect<unknown, never, EndpointServices> =>
463
+ decodeReceipt(encoded).pipe(
464
+ Effect.mapError(protocolFailure("The receipt could not be decoded")),
465
+ Effect.flatMap((receipt) =>
466
+ Effect.gen(function* () {
467
+ const authorizer = yield* OperationAuthorizer;
468
+
469
+ yield* authorizer.authorize(
470
+ OperationAuthorizationRequest.make({
471
+ operation: "awaitSettlement",
472
+ threadId: receipt.threadId,
473
+ submissionId: receipt.submissionId,
474
+ }),
475
+ );
476
+ yield* requireReceiptThread(receipt.threadId);
477
+ yield* requireSubmissionThread(receipt.submissionId);
478
+ const runtime = yield* DurableAgentRuntime;
479
+ const settlement = yield* runtime.awaitSettlement(receipt);
480
+
481
+ return SettlementReached.make({ settlement });
482
+ }),
483
+ ),
484
+ respond,
485
+ Effect.flatMap(encodeResponse),
486
+ );
487
+
488
+ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
489
+ decodeAwaitProgressRequest(encoded).pipe(
490
+ Effect.mapError(protocolFailure("The progress request could not be decoded")),
491
+ Effect.flatMap((request) =>
492
+ Effect.gen(function* () {
493
+ const identity = yield* ThreadObjectIdentity;
494
+ const runtime = yield* DurableAgentRuntime;
495
+ const registry = yield* ProgressWaitRegistry;
496
+
497
+ yield* Effect.scoped(
498
+ Effect.gen(function* () {
499
+ const cancelled = yield* registry.subscribe(
500
+ JSON.stringify([identity.threadId, request.waiterId]),
501
+ );
502
+
503
+ yield* Effect.raceFirst(
504
+ runtime.awaitProgress(identity.threadId, request.afterSequence),
505
+ cancelled,
506
+ );
507
+ }),
508
+ );
509
+
510
+ return ProgressObserved.make();
511
+ }),
512
+ ),
513
+ respond,
514
+ Effect.flatMap(encodeResponse),
515
+ );
516
+
517
+ const cancelProgressEndpoint = (
518
+ encoded: unknown,
519
+ ): Effect.Effect<unknown, never, EndpointServices> =>
520
+ decodeCancelProgressRequest(encoded).pipe(
521
+ Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
522
+ Effect.flatMap((request) =>
523
+ Effect.gen(function* () {
524
+ const identity = yield* ThreadObjectIdentity;
525
+ const registry = yield* ProgressWaitRegistry;
526
+
527
+ yield* registry.cancel(JSON.stringify([identity.threadId, request.waiterId]));
528
+
529
+ return ProgressCancelled.make();
530
+ }),
531
+ ),
532
+ respond,
533
+ Effect.flatMap(encodeResponse),
534
+ );
535
+
536
+ const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
537
+ decodeObservePageRequest(encoded).pipe(
538
+ Effect.mapError(protocolFailure("The observe request could not be decoded")),
539
+ Effect.flatMap((request) =>
540
+ Effect.gen(function* () {
541
+ const identity = yield* ThreadObjectIdentity;
542
+ const store = yield* ThreadStore;
543
+ // The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
544
+ // the default reference preserves the possession behavior.
545
+ const authorizer = yield* OperationAuthorizer;
546
+
547
+ yield* authorizer.authorize(
548
+ OperationAuthorizationRequest.make({
549
+ operation: "observe",
550
+ threadId: identity.threadId,
551
+ }),
552
+ );
553
+
554
+ const records = yield* Stream.runCollect(
555
+ store.read(
556
+ ThreadRead.make({
557
+ threadId: identity.threadId,
558
+ ...(request.afterSequence === undefined
559
+ ? {}
560
+ : { afterSequence: request.afterSequence }),
561
+ limit: request.limit,
562
+ }),
563
+ ),
564
+ );
565
+
566
+ return ObservedPage.make({ records: [...records] });
567
+ }),
568
+ ),
569
+ respond,
570
+ Effect.flatMap(encodeResponse),
571
+ );
572
+
573
+ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
574
+ decodeAbortCommand(encoded).pipe(
575
+ Effect.mapError(protocolFailure("The abort command could not be decoded")),
576
+ Effect.flatMap((command) =>
577
+ Effect.gen(function* () {
578
+ const authorizer = yield* OperationAuthorizer;
579
+
580
+ yield* authorizer.authorize(
581
+ OperationAuthorizationRequest.make({
582
+ operation: "abort",
583
+ submissionId: command.submissionId,
584
+ }),
585
+ );
586
+ yield* requireSubmissionThread(command.submissionId);
587
+ const maintenance = yield* ThreadMaintenance;
588
+ const runtime = yield* DurableAgentRuntime;
589
+ const intent = yield* maintenance.withMutation(runtime.abort(command));
590
+
591
+ return AbortRecorded.make({ intent });
592
+ }),
593
+ ),
594
+ respond,
595
+ Effect.flatMap(encodeResponse),
596
+ );
597
+
598
+ const resolveApprovalEndpoint = (
599
+ encoded: unknown,
600
+ ): Effect.Effect<unknown, never, EndpointServices> =>
601
+ decodeApprovalDecisionCommand(encoded).pipe(
602
+ Effect.mapError(protocolFailure("The approval command could not be decoded")),
603
+ Effect.flatMap((command) =>
604
+ Effect.gen(function* () {
605
+ const authorizer = yield* OperationAuthorizer;
606
+
607
+ yield* authorizer.authorize(
608
+ OperationAuthorizationRequest.make({
609
+ operation: "resolveApproval",
610
+ submissionId: command.submissionId,
611
+ }),
612
+ );
613
+ yield* requireSubmissionThread(command.submissionId);
614
+ const maintenance = yield* ThreadMaintenance;
615
+ const runtime = yield* DurableAgentRuntime;
616
+ const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
617
+
618
+ return ApprovalRecorded.make({ intent });
619
+ }),
620
+ ),
621
+ respond,
622
+ Effect.flatMap(encodeResponse),
623
+ );
624
+
625
+ const resolveUnknownEndpoint = (
626
+ encoded: unknown,
627
+ ): Effect.Effect<unknown, never, EndpointServices> =>
628
+ decodeUnknownResolutionCommand(encoded).pipe(
629
+ Effect.mapError(protocolFailure("The resolution command could not be decoded")),
630
+ Effect.flatMap((command) =>
631
+ Effect.gen(function* () {
632
+ const authorizer = yield* OperationAuthorizer;
633
+
634
+ yield* authorizer.authorize(
635
+ OperationAuthorizationRequest.make({
636
+ operation: "resolveUnknown",
637
+ submissionId: command.submissionId,
638
+ }),
639
+ );
640
+ yield* requireSubmissionThread(command.submissionId);
641
+ const maintenance = yield* ThreadMaintenance;
642
+ const runtime = yield* DurableAgentRuntime;
643
+ const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
644
+
645
+ return UnknownResolutionRecorded.make({ intent });
646
+ }),
647
+ ),
648
+ respond,
649
+ Effect.flatMap(encodeResponse),
650
+ );
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // P7 administrative entry points (plan §3): explain/verify/retry/obligations over the SAME
654
+ // envelope discipline as the host protocol — closed request/response Schema unions, typed
655
+ // failures that re-decode to identical tags, protocol anomalies answered typed. The envelopes
656
+ // live here (not `client.ts`) because no Worker-side client consumption exists yet; `wake`
657
+ // already exists as the `wake()` entry point.
658
+ // ---------------------------------------------------------------------------
659
+
660
+ /** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
661
+ export class AdminExplainRequest extends Schema.Class<AdminExplainRequest>(
662
+ "@effect-agent/platform-cloudflare/AdminExplainRequest",
663
+ )({
664
+ submissionId: Schema.optionalKey(SubmissionId),
665
+ }) {}
666
+
667
+ /** Verify carries no parameters — the addressed Object IS the lane. */
668
+ export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
669
+ "@effect-agent/platform-cloudflare/AdminVerifyRequest",
670
+ )({}) {}
671
+
672
+ /** Every typed failure of the four admin entry points, plus the protocol's own errors. */
673
+ export const AdminFailure = Schema.Union([
674
+ AdmissionPolicyError,
675
+ OperationDenied,
676
+ RetryRefused,
677
+ LedgerError,
678
+ RunJournalError,
679
+ DigestError,
680
+ OwnershipLost,
681
+ SettlementConflict,
682
+ ThreadStoreError,
683
+ ThreadNotMaterialized,
684
+ AppendConflict,
685
+ FenceRejected,
686
+ DurableRuntimeFailpointError,
687
+ DurableAlarmError,
688
+ HostProtocolError,
689
+ ]);
690
+
691
+ export type AdminFailure = typeof AdminFailure.Type;
692
+
693
+ export class ExplainedRecovery extends Schema.TaggedClass<ExplainedRecovery>(
694
+ "@effect-agent/platform-cloudflare/ExplainedRecovery",
695
+ )("ExplainedRecovery", {
696
+ explanations: Schema.Array(RecoveryExplanation).check(Schema.isMaxLength(1_024)),
697
+ }) {}
698
+
699
+ export class VerifiedIntegrity extends Schema.TaggedClass<VerifiedIntegrity>(
700
+ "@effect-agent/platform-cloudflare/VerifiedIntegrity",
701
+ )("VerifiedIntegrity", {
702
+ report: IntegrityReport,
703
+ }) {}
704
+
705
+ export class RetryExecuted extends Schema.TaggedClass<RetryExecuted>(
706
+ "@effect-agent/platform-cloudflare/RetryExecuted",
707
+ )("RetryExecuted", {
708
+ report: RecoveryReport,
709
+ }) {}
710
+
711
+ export class ObligationsScanned extends Schema.TaggedClass<ObligationsScanned>(
712
+ "@effect-agent/platform-cloudflare/ObligationsScanned",
713
+ )("ObligationsScanned", {
714
+ report: ObligationReport,
715
+ }) {}
716
+
717
+ /** The admin entry point failed TYPED on the Object; the failure re-decodes verbatim. */
718
+ export class AdminFailed extends Schema.TaggedClass<AdminFailed>(
719
+ "@effect-agent/platform-cloudflare/AdminFailed",
720
+ )("AdminFailed", {
721
+ failure: AdminFailure,
722
+ }) {}
723
+
724
+ /** The uniform answer of one admin entry point. Callers narrow by the tag their call implies. */
725
+ export const AdminResponse = Schema.Union([
726
+ ExplainedRecovery,
727
+ VerifiedIntegrity,
728
+ RetryExecuted,
729
+ ObligationsScanned,
730
+ AdminFailed,
731
+ ]);
732
+
733
+ export type AdminResponse = typeof AdminResponse.Type;
734
+
735
+ export const decodeAdminExplainRequest = Schema.decodeUnknownEffect(AdminExplainRequest);
736
+ export const decodeAdminVerifyRequest = Schema.decodeUnknownEffect(AdminVerifyRequest);
737
+ export const decodeRetryCommand = Schema.decodeUnknownEffect(RetryCommand);
738
+ export const decodeObligationThresholds = Schema.decodeUnknownEffect(ObligationThresholds);
739
+ export const encodeAdminResponse = Schema.encodeEffect(AdminResponse);
740
+ export const decodeAdminResponse = Schema.decodeUnknownEffect(AdminResponse);
741
+
742
+ /** Fold one admin endpoint's typed failures into the uniform `AdminResponse` envelope. */
743
+ const respondAdmin = <Result extends AdminResponse, Failure extends AdminFailure>(
744
+ effect: Effect.Effect<Result, Failure, EndpointServices>,
745
+ ): Effect.Effect<AdminResponse, never, EndpointServices> =>
746
+ effect.pipe(
747
+ Effect.map((result): AdminResponse => result),
748
+ Effect.catch((failure) => Effect.succeed<AdminResponse>(AdminFailed.make({ failure }))),
749
+ );
750
+
751
+ /** Encode the admin response envelope; an unencodable response degrades to a protocol failure. */
752
+ const encodeAdminResponseTotal = (response: AdminResponse): Effect.Effect<unknown> =>
753
+ encodeAdminResponse(response).pipe(
754
+ Effect.catch((error) =>
755
+ Effect.succeed<unknown>({
756
+ _tag: "AdminFailed",
757
+ failure: {
758
+ _tag: "HostProtocolError",
759
+ message: boundHostDiagnostic(`The admin response could not be encoded: ${error.message}`),
760
+ },
761
+ }),
762
+ ),
763
+ );
764
+
765
+ const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
766
+ decodeAdminExplainRequest(encoded).pipe(
767
+ Effect.mapError(protocolFailure("The explain request could not be decoded")),
768
+ Effect.flatMap((request) =>
769
+ Effect.gen(function* () {
770
+ const identity = yield* ThreadObjectIdentity;
771
+ const runtime = yield* DurableAgentRuntime;
772
+
773
+ const explanations =
774
+ request.submissionId === undefined
775
+ ? yield* runtime.explainThread(identity.threadId)
776
+ : [yield* runtime.explain(request.submissionId)];
777
+
778
+ return ExplainedRecovery.make({ explanations });
779
+ }),
780
+ ),
781
+ respondAdmin,
782
+ Effect.flatMap(encodeAdminResponseTotal),
783
+ );
784
+
785
+ const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
786
+ decodeAdminVerifyRequest(encoded).pipe(
787
+ Effect.mapError(protocolFailure("The verify request could not be decoded")),
788
+ Effect.flatMap(() =>
789
+ Effect.gen(function* () {
790
+ const identity = yield* ThreadObjectIdentity;
791
+ const runtime = yield* DurableAgentRuntime;
792
+ const report = yield* runtime.verify(identity.threadId);
793
+
794
+ return VerifiedIntegrity.make({ report });
795
+ }),
796
+ ),
797
+ respondAdmin,
798
+ Effect.flatMap(encodeAdminResponseTotal),
799
+ );
800
+
801
+ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
802
+ decodeRetryCommand(encoded).pipe(
803
+ Effect.mapError(protocolFailure("The retry command could not be decoded")),
804
+ Effect.flatMap((command) =>
805
+ Effect.gen(function* () {
806
+ const maintenance = yield* ThreadMaintenance;
807
+ const runtime = yield* DurableAgentRuntime;
808
+ // Retry may repair durable state, so its generation + alarm commit before the mutation.
809
+ const report = yield* maintenance.withMutation(runtime.retry(command));
810
+
811
+ return RetryExecuted.make({ report });
812
+ }),
813
+ ),
814
+ respondAdmin,
815
+ Effect.flatMap(encodeAdminResponseTotal),
816
+ );
817
+
818
+ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
819
+ decodeObligationThresholds(encoded).pipe(
820
+ Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")),
821
+ Effect.flatMap((thresholds) =>
822
+ Effect.gen(function* () {
823
+ const runtime = yield* DurableAgentRuntime;
824
+ const report = yield* runtime.scanObligations(thresholds);
825
+
826
+ return ObligationsScanned.make({ report });
827
+ }),
828
+ ),
829
+ respondAdmin,
830
+ Effect.flatMap(encodeAdminResponseTotal),
831
+ );
832
+
833
+ /**
834
+ * Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
835
+ * public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
836
+ * finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
837
+ * alarm so the mutated lane is processed promptly. Protocol anomalies answer
838
+ * `PortFailed(PortProtocolError)`.
839
+ */
840
+ const encodePortResponseTotal = (response: PortResponse) =>
841
+ encodePortResponse(response).pipe(
842
+ Effect.catch((error) =>
843
+ Effect.succeed(
844
+ encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
845
+ ),
846
+ ),
847
+ );
848
+
849
+ const portGuardFailure = (failure: LedgerError | HostProtocolError): PortFailed =>
850
+ PortFailed.make({
851
+ failure:
852
+ failure._tag === "LedgerError"
853
+ ? failure
854
+ : PortProtocolError.make({ message: "The port request is not for the addressed Thread" }),
855
+ });
856
+
857
+ export const portCall = (
858
+ encoded: unknown,
859
+ ): Effect.Effect<
860
+ unknown,
861
+ never,
862
+ ThreadObjectPorts | ThreadMaintenance | DurableAlarmService | ThreadObjectIdentity
863
+ > =>
864
+ Effect.gen(function* () {
865
+ const ports = yield* ThreadObjectPorts;
866
+ const maintenance = yield* ThreadMaintenance;
867
+ const alarm = yield* DurableAlarmService;
868
+
869
+ const decoded = yield* decodePortRequest(encoded).pipe(
870
+ Effect.map((request) => ({ _tag: "success" as const, request })),
871
+ Effect.catch((error) => Effect.succeed({ _tag: "failure" as const, message: error.message })),
872
+ );
873
+
874
+ if (decoded._tag === "failure") {
875
+ return encodedPortProtocolFailure(
876
+ `The port request could not be decoded: ${decoded.message}`,
877
+ );
878
+ }
879
+ if (
880
+ decoded.request._tag === "LedgerLookup" &&
881
+ decoded.request.request._tag === "SubmissionLookupById"
882
+ ) {
883
+ const response = yield* lookupAddressedSubmission(decoded.request.request.submissionId).pipe(
884
+ Effect.map((submission) =>
885
+ PortSucceeded.make({
886
+ result: LedgerLookupResult.make(
887
+ Option.isSome(submission) ? { submission: submission.value } : {},
888
+ ),
889
+ }),
890
+ ),
891
+ Effect.catch((failure) => Effect.succeed(portGuardFailure(failure))),
892
+ );
893
+
894
+ return yield* encodePortResponseTotal(response);
895
+ }
896
+ const identityCheck = yield* requirePortThread(decoded.request).pipe(Effect.result);
897
+
898
+ if (identityCheck._tag === "Failure")
899
+ return yield* encodePortResponseTotal(portGuardFailure(identityCheck.failure));
900
+ const mutating = isMutatingPortRequest(decoded.request);
901
+
902
+ const handled = yield* (
903
+ mutating
904
+ ? maintenance.withMutation(ports.handle(decoded.request))
905
+ : ports.handle(decoded.request)
906
+ ).pipe(Effect.exit);
907
+
908
+ if (handled._tag === "Failure") {
909
+ // Without the committed generation/alarm the invariant cannot be promised; refuse before
910
+ // the port mutation runs. `ports.handle` itself is total, so this is the maintenance error.
911
+ return encodedPortProtocolFailure(
912
+ "The owner Object could not arm its maintenance alarm before the mutation.",
913
+ );
914
+ }
915
+
916
+ const response = yield* encodePortResponseTotal(handled.value);
917
+
918
+ if (mutating) {
919
+ // Prompt processing hint; the pre-armed alarm already guarantees convergence.
920
+ yield* alarm.scheduleNow.pipe(
921
+ Effect.catch((error) =>
922
+ Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error),
923
+ ),
924
+ );
925
+ }
926
+
927
+ return response;
928
+ });
929
+
930
+ const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
931
+ const identity = yield* ThreadObjectIdentity;
932
+ const wake = yield* WakeScheduler;
933
+
934
+ // Route the remote hint through this incarnation's scheduler so scoped progress waiters and
935
+ // the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
936
+ yield* wake.notify(identity.threadId);
937
+ });
938
+
939
+ /** The per-Thread wire operations supported by native and application-owned endpoints. */
940
+ export const ThreadRpcOperation = Schema.Literals([
941
+ "submitEncoded",
942
+ "submissionStatusEncoded",
943
+ "awaitSettlementEncoded",
944
+ "awaitProgressEncoded",
945
+ "cancelProgressEncoded",
946
+ "observePage",
947
+ "abortEncoded",
948
+ "resolveApprovalEncoded",
949
+ "resolveUnknownEncoded",
950
+ "portCall",
951
+ "wake",
952
+ ]);
953
+
954
+ export type ThreadRpcOperation = typeof ThreadRpcOperation.Type;
955
+
956
+ const threadRpc = {
957
+ submitEncoded: submitEndpoint,
958
+ submissionStatusEncoded: submissionStatusEndpoint,
959
+ awaitSettlementEncoded: awaitSettlementEndpoint,
960
+ awaitProgressEncoded: awaitProgressEndpoint,
961
+ cancelProgressEncoded: cancelProgressEndpoint,
962
+ observePage: observePageEndpoint,
963
+ abortEncoded: abortEndpoint,
964
+ resolveApprovalEncoded: resolveApprovalEndpoint,
965
+ resolveUnknownEncoded: resolveUnknownEndpoint,
966
+ portCall,
967
+ wake: () => wakeEndpoint,
968
+ } satisfies Record<
969
+ ThreadRpcOperation,
970
+ (encoded: unknown) => Effect.Effect<unknown, never, EndpointServices>
971
+ >;
972
+
973
+ /**
974
+ * Bind an addressed request to its logical Thread while sharing one physical runtime. This
975
+ * validates local placement before invoking the same native handlers. Receipts, Submission
976
+ * commands and port envelopes must match this identity; progress cancellation is Thread-scoped.
977
+ * The producer comes from the actual runtime configuration, never from caller input. These
978
+ * guards supplement the existing current model/Tool and operation authorization policies.
979
+ */
980
+ export const handleRpc = Effect.fn("ThreadObject.handleRpc")(function* (
981
+ threadId: ThreadId,
982
+ operation: ThreadRpcOperation,
983
+ encoded: unknown,
984
+ ) {
985
+ const placement = yield* ThreadObjectPlacement;
986
+
987
+ if (!placement.ownsThread(threadId))
988
+ return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
989
+ const { producerId } = yield* DurableRuntimeConfig;
990
+
991
+ return yield* threadRpc[operation](encoded).pipe(
992
+ Effect.provideService(ThreadObjectIdentity, { threadId, producerId }),
993
+ );
994
+ });
995
+
996
+ const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
997
+ function* () {
998
+ const maintenance = yield* ThreadMaintenance;
999
+
1000
+ // Typed pass failures propagate: the rejected promise makes workerd retry the alarm
1001
+ // (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
1002
+ yield* maintenance.pass;
1003
+ },
1004
+ );
1005
+
1006
+ const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
1007
+ function* () {
1008
+ // Forcing ThreadMaintenance forces the whole Layer stack: migration + exact-version
1009
+ // check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
1010
+ // the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
1011
+ const maintenance = yield* ThreadMaintenance;
1012
+
1013
+ yield* maintenance.ensureAlarm;
1014
+ },
1015
+ );
1016
+
1017
+ /**
1018
+ * Adapter from effect-cf's native Durable Object services to Effect Agent's existing platform
1019
+ * ports. effect-cf owns the cached ManagedRuntime and supplies these values once per Object
1020
+ * incarnation; the durable runtime continues to depend only on the narrow services below.
1021
+ */
1022
+ const effectCfPlatformLayer = (
1023
+ namespaceBinding: string,
1024
+ rpcTracing = false,
1025
+ ): Layer.Layer<
1026
+ DurableObjectContext | ThreadObjectNamespace,
1027
+ CloudflareBindingError,
1028
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
1029
+ > => {
1030
+ const context = Layer.effect(DurableObjectContext)(
1031
+ Effect.gen(function* () {
1032
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
1033
+ const env = yield* WorkerEnvironment;
1034
+
1035
+ return DurableObjectContext.of({ ctx: state.raw, env });
1036
+ }),
1037
+ );
1038
+
1039
+ const namespace = Layer.effect(ThreadObjectNamespace)(
1040
+ Effect.gen(function* () {
1041
+ const env = yield* WorkerEnvironment;
1042
+ const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
1043
+
1044
+ return ThreadObjectNamespace.of({
1045
+ get: (threadId) => binding.get(binding.idFromName(threadId)),
1046
+ ...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
1047
+ });
1048
+ }),
1049
+ );
1050
+
1051
+ return Layer.merge(context, namespace);
1052
+ };
1053
+
1054
+ /** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
1055
+ export interface Instance<EventServices = never> extends InstanceType<
1056
+ EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
1057
+ > {
1058
+ submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1059
+ submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1060
+ awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1061
+ awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1062
+ cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1063
+ observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1064
+ abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1065
+ resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1066
+ resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
1067
+ explainEncoded(encoded: unknown): Promise<unknown>;
1068
+ verifyEncoded(encoded: unknown): Promise<unknown>;
1069
+ retryEncoded(encoded: unknown): Promise<unknown>;
1070
+ obligationsEncoded(encoded: unknown): Promise<unknown>;
1071
+ portCall(encoded: unknown): Promise<unknown>;
1072
+ wake(): Promise<void>;
1073
+ alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
1074
+ }
1075
+
1076
+ /** The constructor shape workerd instantiates for each Thread Object. */
1077
+ export interface Class<EventServices = never> {
1078
+ new (ctx: DurableObjectState, env: Cloudflare.Env): Instance<EventServices>;
1079
+ }
1080
+
1081
+ /**
1082
+ * Export a composed application Layer as a native Durable Object class.
1083
+ * Bootstrap services are provided to the whole graph before it acquires, so application Layers
1084
+ * can yield effect-cf's WorkerEnvironment and DurableObjectState, derived identity, and Crypto.
1085
+ * Effect Config reads scalar Worker vars and secrets through effect-cf's environment provider;
1086
+ * WorkerEnvironment exposes resource bindings without a separate config Layer.
1087
+ * Application dependencies remain visible until Layer.provide satisfies them. effect-cf owns the
1088
+ * cached ManagedRuntime, native RPC methods, event scopes, and telemetry flushing.
1089
+ * Initialization is local and bounded inside the constructor gate. Cloudflare eviction does not
1090
+ * guarantee finalizers; put resources requiring timely release in scoped operations or eventLayer.
1091
+ */
1092
+ export const make = <
1093
+ ApplicationServices,
1094
+ ApplicationError,
1095
+ EventServices = never,
1096
+ EventLayerError = never,
1097
+ >(
1098
+ applicationLayer: Layer.Layer<
1099
+ CloudflareDurableRuntimeServices | ApplicationServices,
1100
+ ApplicationError,
1101
+ | CloudflareBootstrapServices
1102
+ | EffectCfDurableObjectState.DurableObjectState
1103
+ | WorkerEnvironment
1104
+ | DurableObjectContext
1105
+ | ThreadObjectNamespace
1106
+ >,
1107
+ options: Options<ApplicationServices, EventServices, EventLayerError>,
1108
+ ): Class<ApplicationServices | EventServices> => {
1109
+ const application = applicationLayer.pipe(
1110
+ Layer.provideMerge(layerConfig(options)),
1111
+ Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
1112
+ );
1113
+
1114
+ // The storage/config Layer must acquire inside Cloudflare's constructor gate. effect-cf owns
1115
+ // the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
1116
+ // before migration, compatibility checks, or alarm inspection touch Object storage.
1117
+ const runtime: Layer.Layer<
1118
+ RuntimeServices | ApplicationServices,
1119
+ ThreadObjectInitializationError | ApplicationError,
1120
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
1121
+ > = Layer.effectContext(
1122
+ Effect.gen(function* () {
1123
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
1124
+ const scope = yield* Effect.scope;
1125
+
1126
+ return yield* state.blockConcurrencyWhile(
1127
+ Effect.gen(function* () {
1128
+ const services = yield* Layer.buildWithScope(application, scope);
1129
+
1130
+ yield* gateEndpoint.pipe(Effect.provide(services));
1131
+
1132
+ return services;
1133
+ }),
1134
+ );
1135
+ }),
1136
+ );
1137
+
1138
+ const rpc = {
1139
+ ...threadRpc,
1140
+ explainEncoded: (encoded: unknown) => explainEndpoint(encoded),
1141
+ verifyEncoded: (encoded: unknown) => verifyEndpoint(encoded),
1142
+ retryEncoded: (encoded: unknown) => retryEndpoint(encoded),
1143
+ obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
1144
+ } satisfies EffectCfDurableObject.DurableObjectRpc<
1145
+ RuntimeServices | ApplicationServices | EventServices
1146
+ >;
1147
+
1148
+ type NativeOptions = EffectCfDurableObject.DurableObjectOptions<
1149
+ RuntimeServices | ApplicationServices,
1150
+ EventServices,
1151
+ EventLayerError,
1152
+ typeof rpc
1153
+ >;
1154
+
1155
+ const EffectCfThreadObject = EffectCfDurableObject.make<
1156
+ RuntimeServices | ApplicationServices,
1157
+ ThreadObjectInitializationError | ApplicationError,
1158
+ EventServices,
1159
+ EventLayerError,
1160
+ typeof rpc
1161
+ >(runtime, {
1162
+ ...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
1163
+ ...(options.eventLayer === undefined ? {} : { eventLayer: options.eventLayer }),
1164
+ // Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
1165
+ // in each bounded pass so cross-Object initialization cannot deadlock.
1166
+ initialize: Effect.void,
1167
+ rpc,
1168
+ alarm: () => alarmEndpoint,
1169
+ // This host owns the raw alarm and supplies event services through options.eventLayer.
1170
+ // Upstream's conditional alarm-registration check cannot reduce over generic application
1171
+ // services. Options and the rpc satisfies check above retain their Effect requirements.
1172
+ } as NativeOptions);
1173
+
1174
+ // effect-cf's class type keeps `alarm` optional even when the handler option is present. This
1175
+ // concrete override reflects this factory's stronger contract while delegating execution to
1176
+ // the effect-cf runtime unchanged.
1177
+ class ThreadObject extends EffectCfThreadObject {
1178
+ override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
1179
+ return super.alarm?.(alarmInfo);
1180
+ }
1181
+ }
1182
+
1183
+ return ThreadObject;
1184
+ };