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

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