@effect-agent/platform-cloudflare 0.1.0-beta.79 → 0.1.0-beta.80

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 (33) hide show
  1. package/dist/Alarm.d.mts +17 -5
  2. package/dist/Alarm.mjs +48 -12
  3. package/dist/Alarm.mjs.map +1 -1
  4. package/dist/CloudflareBindings.d.mts +14 -7
  5. package/dist/CloudflareBindings.mjs +10 -7
  6. package/dist/CloudflareBindings.mjs.map +1 -1
  7. package/dist/CloudflareScheduling.mjs +2 -2
  8. package/dist/CloudflareSubscriptions.mjs +1 -1
  9. package/dist/CloudflareThreadClient.d.mts +41 -41
  10. package/dist/CloudflareThreadClient.mjs +3 -3
  11. package/dist/CloudflareThreadClient.mjs.map +1 -1
  12. package/dist/{ThreadObject-MuU9xAyn.d.mts → ThreadObject-3uDpKfB3.d.mts} +82 -44
  13. package/dist/{ThreadObject-KJf_5Isr.mjs → ThreadObject-DGkXv41C.mjs} +213 -68
  14. package/dist/ThreadObject-DGkXv41C.mjs.map +1 -0
  15. package/dist/ThreadObject.d.mts +2 -2
  16. package/dist/ThreadObject.mjs +2 -2
  17. package/dist/WakeScheduler.d.mts +2 -2
  18. package/dist/WakeScheduler.mjs +6 -6
  19. package/dist/WakeScheduler.mjs.map +1 -1
  20. package/dist/index.d.mts +1 -1
  21. package/dist/index.mjs +1 -1
  22. package/dist/{prepared-admission-G7N4DDqS.mjs → prepared-admission-DSi55W-7.mjs} +2 -2
  23. package/dist/{prepared-admission-G7N4DDqS.mjs.map → prepared-admission-DSi55W-7.mjs.map} +1 -1
  24. package/package.json +1 -1
  25. package/src/Alarm.ts +91 -14
  26. package/src/CloudflareBindings.ts +15 -6
  27. package/src/CloudflareThreadClient.ts +3 -3
  28. package/src/ThreadObject.ts +284 -59
  29. package/src/WakeScheduler.ts +6 -6
  30. package/src/internal/layers.ts +150 -22
  31. package/src/internal/message-delivery.ts +37 -21
  32. package/src/internal/transport.ts +2 -2
  33. package/dist/ThreadObject-KJf_5Isr.mjs.map +0 -1
@@ -1,9 +1,14 @@
1
- import { type AgentId } from "@effect-agent/core/Identifiers";
1
+ import { type AgentId, type ThreadId } from "@effect-agent/core/Identifiers";
2
2
  import { SubmissionId } from "@effect-agent/core/Identifiers";
3
3
  import {
4
4
  decodePortRequest,
5
5
  encodePortResponse,
6
+ LedgerLookupResult,
7
+ PortFailed,
8
+ PortProtocolError,
9
+ PortSucceeded,
6
10
  type PortRequest,
11
+ type PortResponse,
7
12
  } from "@effect-agent/storage-cloudflare/PortProtocol";
8
13
  import {
9
14
  IntegrityReport,
@@ -16,6 +21,7 @@ import {
16
21
  import { DigestError } from "@effect-agent/thread/Digest";
17
22
  import {
18
23
  DurableAgentRuntime,
24
+ DurableRuntimeConfig,
19
25
  RecoveryReport,
20
26
  type DurableSubmitAgent,
21
27
  } from "@effect-agent/thread/DurableAgentRuntime";
@@ -55,10 +61,13 @@ import {
55
61
  ThreadMaintenance,
56
62
  DurableAlarmError,
57
63
  DurableAlarmService,
64
+ ThreadMutationGate,
65
+ publishCommitted,
58
66
  type MaintenancePassFailure,
59
67
  } from "./Alarm.ts";
60
68
  import {
61
69
  ThreadObjectIdentity,
70
+ ThreadObjectPlacement,
62
71
  DurableObjectContext,
63
72
  ThreadObjectNamespace,
64
73
  threadNamespaceFromEnv,
@@ -89,6 +98,7 @@ import {
89
98
  encodeHostResponse,
90
99
  type HostFailure,
91
100
  type HostResponse,
101
+ type SubmitRequest,
92
102
  } from "./CloudflareThreadClient.ts";
93
103
  import {
94
104
  layerConfig,
@@ -103,6 +113,9 @@ import { ProgressWaitRegistry } from "./internal/progress-wait.ts";
103
113
  export {
104
114
  layer,
105
115
  layerConfig,
116
+ layerHostConfig,
117
+ layerInHost,
118
+ ThreadObjectPorts,
106
119
  type ThreadPublicationOptions as PublicationOptions,
107
120
  type CloudflareDurableRuntimeOptions as RuntimeOptions,
108
121
  type CloudflareDurableRuntimeServices as Services,
@@ -231,19 +244,21 @@ const utf8Bytes = (value: PersistedJson): number =>
231
244
  * exempt: its accepted-work obligation already exists, and returning the original Receipt
232
245
  * consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
233
246
  */
234
- const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request: {
235
- readonly principal: SubmissionLookupByKey["principal"];
236
- readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
237
- readonly inputPayload: PersistedJson;
238
- }) {
239
- const identity = yield* ThreadObjectIdentity;
247
+ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (
248
+ threadId: ThreadId,
249
+ request: {
250
+ readonly principal: SubmissionLookupByKey["principal"];
251
+ readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
252
+ readonly inputPayload: PersistedJson;
253
+ },
254
+ ) {
240
255
  const config = yield* CloudflareDurableRuntimeConfig;
241
256
  const ledger = yield* SubmissionLedger;
242
257
  const { ctx } = yield* DurableObjectContext;
243
258
 
244
259
  const existing = yield* ledger.lookup(
245
260
  SubmissionLookupByKey.make({
246
- threadId: identity.threadId,
261
+ threadId,
247
262
  principal: request.principal,
248
263
  idempotencyKey: request.idempotencyKey,
249
264
  }),
@@ -261,8 +276,10 @@ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(functi
261
276
  });
262
277
  }
263
278
 
264
- // One Thread per Object (durability §5): the local scan IS this lane's queue.
265
- const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
279
+ const nonterminal = yield* ledger.scanNonterminal.pipe(
280
+ Stream.filter((submission) => submission.threadId === threadId),
281
+ Stream.runCollect,
282
+ );
266
283
 
267
284
  if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
268
285
  return yield* AdmissionLimitExceeded.make({
@@ -296,39 +313,110 @@ const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof Per
296
313
  },
297
314
  });
298
315
 
316
+ /** A physical owner may hold other Threads; an addressed request cannot act on their IDs. */
317
+ const lookupAddressedSubmission = Effect.fn("ThreadObject.lookupAddressedSubmission")(function* (
318
+ submissionId: SubmissionId,
319
+ ) {
320
+ const { threadId } = yield* ThreadObjectIdentity;
321
+ const ports = yield* ThreadObjectPorts;
322
+ const submission = yield* ports.lookupSubmission(submissionId);
323
+
324
+ if (Option.isSome(submission) && submission.value.threadId !== threadId)
325
+ return yield* HostProtocolError.make({ message: "The Submission belongs to another Thread" });
326
+
327
+ return submission;
328
+ });
329
+
330
+ const requireSubmissionThread = Effect.fn("ThreadObject.requireSubmissionThread")(function* (
331
+ submissionId: SubmissionId,
332
+ ) {
333
+ const submission = yield* lookupAddressedSubmission(submissionId);
334
+
335
+ if (Option.isNone(submission))
336
+ return yield* LedgerError.make({
337
+ operation: "addressed Submission lookup",
338
+ message: "The addressed Thread has no such Submission",
339
+ });
340
+ });
341
+
342
+ const requireReceiptThread = Effect.fn("ThreadObject.requireReceiptThread")(function* (
343
+ threadId: ThreadId,
344
+ ) {
345
+ const identity = yield* ThreadObjectIdentity;
346
+
347
+ if (threadId !== identity.threadId)
348
+ return yield* HostProtocolError.make({ message: "The Receipt belongs to another Thread" });
349
+ });
350
+
351
+ const requirePortThread = (request: PortRequest) => {
352
+ switch (request._tag) {
353
+ case "LedgerLookup":
354
+ return request.request._tag === "SubmissionLookupById"
355
+ ? lookupAddressedSubmission(request.request.submissionId).pipe(Effect.asVoid)
356
+ : requireReceiptThread(request.request.threadId);
357
+ case "LedgerMarkReady":
358
+ case "LedgerRequestAbort":
359
+ return requireSubmissionThread(request.request.submissionId);
360
+ case "LedgerRecordChildSettled":
361
+ return requireSubmissionThread(request.request.parentSubmissionId);
362
+ case "LedgerAdmit":
363
+ case "LedgerResolveAdmission":
364
+ case "StoreMaterialize":
365
+ case "StoreAppend":
366
+ case "StoreReadPage":
367
+ case "StoreInspectTail":
368
+ case "StoreExport":
369
+ return requireReceiptThread(request.request.threadId);
370
+ }
371
+ request satisfies never;
372
+ };
373
+
374
+ /**
375
+ * Admit an already Schema-decoded request to a logical Thread in this physical owner.
376
+ * Custom hosts validate local placement before calling this Effect and provide their same
377
+ * runtime/maintenance instances. The native endpoint uses this path too: queue limits,
378
+ * idempotent receipts and the pre-admission generation/alarm commit have one owner.
379
+ */
380
+ export const submit = Effect.fn("ThreadObject.submit")(function* (
381
+ threadId: ThreadId,
382
+ request: SubmitRequest,
383
+ ) {
384
+ const placement = yield* ThreadObjectPlacement;
385
+
386
+ if (!placement.ownsThread(threadId))
387
+ return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
388
+ const mutations = yield* ThreadMutationGate;
389
+ const runtime = yield* DurableAgentRuntime;
390
+
391
+ yield* gateAdmissionLimits(threadId, request);
392
+
393
+ return yield* mutations.withMutation(
394
+ runtime
395
+ .submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
396
+ threadId,
397
+ principal: request.principal,
398
+ idempotencyKey: request.idempotencyKey,
399
+ ...(request.admissionGroup === undefined ? {} : { admissionGroup: request.admissionGroup }),
400
+ ...(request.admissionFence === undefined ? {} : { admissionFence: request.admissionFence }),
401
+ ...(request.workerAdmission === undefined
402
+ ? {}
403
+ : { workerAdmission: request.workerAdmission }),
404
+ ...(request.messageAdmission === undefined
405
+ ? {}
406
+ : { messageAdmission: request.messageAdmission }),
407
+ definitions: request.definitions,
408
+ })
409
+ .pipe(Effect.tap(() => publishCommitted)),
410
+ );
411
+ });
412
+
299
413
  const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
300
414
  decodeSubmitRequest(encoded).pipe(
301
415
  Effect.mapError(protocolFailure("The submit request could not be decoded")),
302
416
  Effect.flatMap((request) =>
303
417
  Effect.gen(function* () {
304
418
  const identity = yield* ThreadObjectIdentity;
305
- const maintenance = yield* ThreadMaintenance;
306
- const runtime = yield* DurableAgentRuntime;
307
-
308
- yield* gateAdmissionLimits(request);
309
-
310
- // Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
311
- // cannot acknowledge that generation until this mutation leaves its public RPC seam.
312
- const receipt = yield* maintenance.withMutation(
313
- runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
314
- threadId: identity.threadId,
315
- principal: request.principal,
316
- idempotencyKey: request.idempotencyKey,
317
- ...(request.admissionGroup === undefined
318
- ? {}
319
- : { admissionGroup: request.admissionGroup }),
320
- ...(request.admissionFence === undefined
321
- ? {}
322
- : { admissionFence: request.admissionFence }),
323
- ...(request.workerAdmission === undefined
324
- ? {}
325
- : { workerAdmission: request.workerAdmission }),
326
- ...(request.messageAdmission === undefined
327
- ? {}
328
- : { messageAdmission: request.messageAdmission }),
329
- definitions: request.definitions,
330
- }),
331
- );
419
+ const receipt = yield* submit(identity.threadId, request);
332
420
 
333
421
  return SubmitSucceeded.make({ receipt });
334
422
  }),
@@ -344,6 +432,17 @@ const submissionStatusEndpoint = (
344
432
  Effect.mapError(protocolFailure("The receipt could not be decoded")),
345
433
  Effect.flatMap((receipt) =>
346
434
  Effect.gen(function* () {
435
+ const authorizer = yield* OperationAuthorizer;
436
+
437
+ yield* authorizer.authorize(
438
+ OperationAuthorizationRequest.make({
439
+ operation: "awaitSettlement",
440
+ threadId: receipt.threadId,
441
+ submissionId: receipt.submissionId,
442
+ }),
443
+ );
444
+ yield* requireReceiptThread(receipt.threadId);
445
+ yield* requireSubmissionThread(receipt.submissionId);
347
446
  const runtime = yield* DurableAgentRuntime;
348
447
 
349
448
  return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
@@ -360,6 +459,17 @@ const awaitSettlementEndpoint = (
360
459
  Effect.mapError(protocolFailure("The receipt could not be decoded")),
361
460
  Effect.flatMap((receipt) =>
362
461
  Effect.gen(function* () {
462
+ const authorizer = yield* OperationAuthorizer;
463
+
464
+ yield* authorizer.authorize(
465
+ OperationAuthorizationRequest.make({
466
+ operation: "awaitSettlement",
467
+ threadId: receipt.threadId,
468
+ submissionId: receipt.submissionId,
469
+ }),
470
+ );
471
+ yield* requireReceiptThread(receipt.threadId);
472
+ yield* requireSubmissionThread(receipt.submissionId);
363
473
  const runtime = yield* DurableAgentRuntime;
364
474
  const settlement = yield* runtime.awaitSettlement(receipt);
365
475
 
@@ -381,7 +491,9 @@ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never,
381
491
 
382
492
  yield* Effect.scoped(
383
493
  Effect.gen(function* () {
384
- const cancelled = yield* registry.subscribe(request.waiterId);
494
+ const cancelled = yield* registry.subscribe(
495
+ JSON.stringify([identity.threadId, request.waiterId]),
496
+ );
385
497
 
386
498
  yield* Effect.raceFirst(
387
499
  runtime.awaitProgress(identity.threadId, request.afterSequence),
@@ -404,9 +516,10 @@ const cancelProgressEndpoint = (
404
516
  Effect.mapError(protocolFailure("The progress cancellation could not be decoded")),
405
517
  Effect.flatMap((request) =>
406
518
  Effect.gen(function* () {
519
+ const identity = yield* ThreadObjectIdentity;
407
520
  const registry = yield* ProgressWaitRegistry;
408
521
 
409
- yield* registry.cancel(request.waiterId);
522
+ yield* registry.cancel(JSON.stringify([identity.threadId, request.waiterId]));
410
523
 
411
524
  return ProgressCancelled.make();
412
525
  }),
@@ -457,6 +570,15 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
457
570
  Effect.mapError(protocolFailure("The abort command could not be decoded")),
458
571
  Effect.flatMap((command) =>
459
572
  Effect.gen(function* () {
573
+ const authorizer = yield* OperationAuthorizer;
574
+
575
+ yield* authorizer.authorize(
576
+ OperationAuthorizationRequest.make({
577
+ operation: "abort",
578
+ submissionId: command.submissionId,
579
+ }),
580
+ );
581
+ yield* requireSubmissionThread(command.submissionId);
460
582
  const maintenance = yield* ThreadMaintenance;
461
583
  const runtime = yield* DurableAgentRuntime;
462
584
  const intent = yield* maintenance.withMutation(runtime.abort(command));
@@ -475,6 +597,15 @@ const resolveApprovalEndpoint = (
475
597
  Effect.mapError(protocolFailure("The approval command could not be decoded")),
476
598
  Effect.flatMap((command) =>
477
599
  Effect.gen(function* () {
600
+ const authorizer = yield* OperationAuthorizer;
601
+
602
+ yield* authorizer.authorize(
603
+ OperationAuthorizationRequest.make({
604
+ operation: "resolveApproval",
605
+ submissionId: command.submissionId,
606
+ }),
607
+ );
608
+ yield* requireSubmissionThread(command.submissionId);
478
609
  const maintenance = yield* ThreadMaintenance;
479
610
  const runtime = yield* DurableAgentRuntime;
480
611
  const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
@@ -493,6 +624,15 @@ const resolveUnknownEndpoint = (
493
624
  Effect.mapError(protocolFailure("The resolution command could not be decoded")),
494
625
  Effect.flatMap((command) =>
495
626
  Effect.gen(function* () {
627
+ const authorizer = yield* OperationAuthorizer;
628
+
629
+ yield* authorizer.authorize(
630
+ OperationAuthorizationRequest.make({
631
+ operation: "resolveUnknown",
632
+ submissionId: command.submissionId,
633
+ }),
634
+ );
635
+ yield* requireSubmissionThread(command.submissionId);
496
636
  const maintenance = yield* ThreadMaintenance;
497
637
  const runtime = yield* DurableAgentRuntime;
498
638
  const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
@@ -692,7 +832,30 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
692
832
  * alarm so the mutated lane is processed promptly. Protocol anomalies answer
693
833
  * `PortFailed(PortProtocolError)`.
694
834
  */
695
- const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
835
+ const encodePortResponseTotal = (response: PortResponse) =>
836
+ encodePortResponse(response).pipe(
837
+ Effect.catch((error) =>
838
+ Effect.succeed(
839
+ encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
840
+ ),
841
+ ),
842
+ );
843
+
844
+ const portGuardFailure = (failure: LedgerError | HostProtocolError): PortFailed =>
845
+ PortFailed.make({
846
+ failure:
847
+ failure._tag === "LedgerError"
848
+ ? failure
849
+ : PortProtocolError.make({ message: "The port request is not for the addressed Thread" }),
850
+ });
851
+
852
+ export const portCall = (
853
+ encoded: unknown,
854
+ ): Effect.Effect<
855
+ unknown,
856
+ never,
857
+ ThreadObjectPorts | ThreadMaintenance | DurableAlarmService | ThreadObjectIdentity
858
+ > =>
696
859
  Effect.gen(function* () {
697
860
  const ports = yield* ThreadObjectPorts;
698
861
  const maintenance = yield* ThreadMaintenance;
@@ -708,6 +871,27 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
708
871
  `The port request could not be decoded: ${decoded.message}`,
709
872
  );
710
873
  }
874
+ if (
875
+ decoded.request._tag === "LedgerLookup" &&
876
+ decoded.request.request._tag === "SubmissionLookupById"
877
+ ) {
878
+ const response = yield* lookupAddressedSubmission(decoded.request.request.submissionId).pipe(
879
+ Effect.map((submission) =>
880
+ PortSucceeded.make({
881
+ result: LedgerLookupResult.make(
882
+ Option.isSome(submission) ? { submission: submission.value } : {},
883
+ ),
884
+ }),
885
+ ),
886
+ Effect.catch((failure) => Effect.succeed(portGuardFailure(failure))),
887
+ );
888
+
889
+ return yield* encodePortResponseTotal(response);
890
+ }
891
+ const identityCheck = yield* requirePortThread(decoded.request).pipe(Effect.result);
892
+
893
+ if (identityCheck._tag === "Failure")
894
+ return yield* encodePortResponseTotal(portGuardFailure(identityCheck.failure));
711
895
  const mutating = isMutatingPortRequest(decoded.request);
712
896
 
713
897
  const handled = yield* (
@@ -724,13 +908,7 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
724
908
  );
725
909
  }
726
910
 
727
- const response = yield* encodePortResponse(handled.value).pipe(
728
- Effect.catch((error) =>
729
- Effect.succeed(
730
- encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
731
- ),
732
- ),
733
- );
911
+ const response = yield* encodePortResponseTotal(handled.value);
734
912
 
735
913
  if (mutating) {
736
914
  // Prompt processing hint; the pre-armed alarm already guarantees convergence.
@@ -753,6 +931,63 @@ const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(fu
753
931
  yield* wake.notify(identity.threadId);
754
932
  });
755
933
 
934
+ /** The per-Thread wire operations supported by native and application-owned endpoints. */
935
+ export const ThreadRpcOperation = Schema.Literals([
936
+ "submitEncoded",
937
+ "submissionStatusEncoded",
938
+ "awaitSettlementEncoded",
939
+ "awaitProgressEncoded",
940
+ "cancelProgressEncoded",
941
+ "observePage",
942
+ "abortEncoded",
943
+ "resolveApprovalEncoded",
944
+ "resolveUnknownEncoded",
945
+ "portCall",
946
+ "wake",
947
+ ]);
948
+
949
+ export type ThreadRpcOperation = typeof ThreadRpcOperation.Type;
950
+
951
+ const threadRpc = {
952
+ submitEncoded: submitEndpoint,
953
+ submissionStatusEncoded: submissionStatusEndpoint,
954
+ awaitSettlementEncoded: awaitSettlementEndpoint,
955
+ awaitProgressEncoded: awaitProgressEndpoint,
956
+ cancelProgressEncoded: cancelProgressEndpoint,
957
+ observePage: observePageEndpoint,
958
+ abortEncoded: abortEndpoint,
959
+ resolveApprovalEncoded: resolveApprovalEndpoint,
960
+ resolveUnknownEncoded: resolveUnknownEndpoint,
961
+ portCall,
962
+ wake: () => wakeEndpoint,
963
+ } satisfies Record<
964
+ ThreadRpcOperation,
965
+ (encoded: unknown) => Effect.Effect<unknown, never, EndpointServices>
966
+ >;
967
+
968
+ /**
969
+ * Bind an addressed request to its logical Thread while sharing one physical runtime. This
970
+ * validates local placement before invoking the same native handlers. Receipts, Submission
971
+ * commands and port envelopes must match this identity; progress cancellation is Thread-scoped.
972
+ * The producer comes from the actual runtime configuration, never from caller input. These
973
+ * guards supplement the existing current model/Tool and operation authorization policies.
974
+ */
975
+ export const handleRpc = Effect.fn("ThreadObject.handleRpc")(function* (
976
+ threadId: ThreadId,
977
+ operation: ThreadRpcOperation,
978
+ encoded: unknown,
979
+ ) {
980
+ const placement = yield* ThreadObjectPlacement;
981
+
982
+ if (!placement.ownsThread(threadId))
983
+ return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
984
+ const { producerId } = yield* DurableRuntimeConfig;
985
+
986
+ return yield* threadRpc[operation](encoded).pipe(
987
+ Effect.provideService(ThreadObjectIdentity, { threadId, producerId }),
988
+ );
989
+ });
990
+
756
991
  const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
757
992
  function* () {
758
993
  const maintenance = yield* ThreadMaintenance;
@@ -802,7 +1037,7 @@ const effectCfPlatformLayer = (
802
1037
  const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
803
1038
 
804
1039
  return ThreadObjectNamespace.of({
805
- namespace: binding,
1040
+ get: (threadId) => binding.get(binding.idFromName(threadId)),
806
1041
  ...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
807
1042
  });
808
1043
  }),
@@ -896,21 +1131,11 @@ export const make = <
896
1131
  );
897
1132
 
898
1133
  const rpc = {
899
- submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
900
- submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
901
- awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
902
- awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
903
- cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),
904
- observePage: (encoded: unknown) => observePageEndpoint(encoded),
905
- abortEncoded: (encoded: unknown) => abortEndpoint(encoded),
906
- resolveApprovalEncoded: (encoded: unknown) => resolveApprovalEndpoint(encoded),
907
- resolveUnknownEncoded: (encoded: unknown) => resolveUnknownEndpoint(encoded),
1134
+ ...threadRpc,
908
1135
  explainEncoded: (encoded: unknown) => explainEndpoint(encoded),
909
1136
  verifyEncoded: (encoded: unknown) => verifyEndpoint(encoded),
910
1137
  retryEncoded: (encoded: unknown) => retryEndpoint(encoded),
911
1138
  obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
912
- portCall: (encoded: unknown) => portCallEndpoint(encoded),
913
- wake: () => wakeEndpoint,
914
1139
  } satisfies EffectCfDurableObject.DurableObjectRpc<
915
1140
  RuntimeServices | ApplicationServices | EventServices
916
1141
  >;
@@ -3,7 +3,7 @@ import { makeWakeSubscriptionHub, WakeScheduler } from "@effect-agent/thread/Wak
3
3
  import { Effect, Layer, PubSub, Schema, Stream } from "effect";
4
4
 
5
5
  import { DurableAlarmService } from "./Alarm.ts";
6
- import { ThreadObjectIdentity, ThreadObjectNamespace } from "./CloudflareBindings.ts";
6
+ import { ThreadObjectPlacement, ThreadObjectNamespace } from "./CloudflareBindings.ts";
7
7
  import { safeCauseMessage } from "./internal/boundary.ts";
8
8
 
9
9
  /**
@@ -36,12 +36,12 @@ class RemoteWakeDropped extends Schema.TaggedError<RemoteWakeDropped>()("RemoteW
36
36
  export const cloudflareWakeSchedulerLayer: Layer.Layer<
37
37
  WakeScheduler,
38
38
  never,
39
- DurableAlarmService | ThreadObjectIdentity | ThreadObjectNamespace
39
+ DurableAlarmService | ThreadObjectPlacement | ThreadObjectNamespace
40
40
  > = Layer.effect(WakeScheduler)(
41
41
  Effect.gen(function* () {
42
42
  const alarm = yield* DurableAlarmService;
43
- const identity = yield* ThreadObjectIdentity;
44
- const { namespace } = yield* ThreadObjectNamespace;
43
+ const placement = yield* ThreadObjectPlacement;
44
+ const { get } = yield* ThreadObjectNamespace;
45
45
  const hints = yield* PubSub.sliding<ThreadId>(WAKE_BUFFER_CAPACITY);
46
46
  const progress = yield* makeWakeSubscriptionHub;
47
47
 
@@ -61,7 +61,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
61
61
 
62
62
  const notifyRemote = (threadId: ThreadId) =>
63
63
  Effect.tryPromise({
64
- try: () => namespace.get(namespace.idFromName(threadId)).wake(),
64
+ try: () => get(threadId).wake(),
65
65
  catch: (cause) =>
66
66
  RemoteWakeDropped.make({
67
67
  threadId,
@@ -77,7 +77,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
77
77
 
78
78
  return WakeScheduler.of({
79
79
  notify: (threadId) =>
80
- threadId === identity.threadId ? notifyLocal(threadId) : notifyRemote(threadId),
80
+ placement.ownsThread(threadId) ? notifyLocal(threadId) : notifyRemote(threadId),
81
81
  subscribe: progress.subscribe,
82
82
  wakes: Stream.fromPubSub(hints),
83
83
  });