@effect-agent/platform-cloudflare 0.1.0-beta.50 → 0.1.0-beta.52

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.
@@ -13,6 +13,7 @@ import {
13
13
  type SubscriptionAuthorizer,
14
14
  SubscriptionConfiguration,
15
15
  SubscriptionDeliverySnapshot,
16
+ SubscriptionDeliveryKey,
16
17
  SubscriptionError,
17
18
  SubscriptionFailpointError,
18
19
  SubscriptionKey,
@@ -30,7 +31,7 @@ import {
30
31
  } from "@effect-agent/thread/Subscriptions";
31
32
  import { BrowserCrypto } from "@effect/platform-browser";
32
33
  import { SqliteClient } from "@effect/sql-sqlite-do";
33
- import { Cause, Clock, Context, DateTime, Effect, Layer, Schema } from "effect";
34
+ import { Cause, Clock, Context, DateTime, Effect, Layer, Schema, type Scope } from "effect";
34
35
  import {
35
36
  DurableObject as EffectCfDurableObject,
36
37
  DurableObjectAlarm,
@@ -45,6 +46,8 @@ import { cloudflarePreparedInputAdmissionLayer } from "./internal/prepared-admis
45
46
  const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
46
47
  const SUBSCRIPTION_ALARM_ID = "driver";
47
48
  const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
49
+ const MAX_ALARMS_PER_INVOCATION = 16;
50
+ const MAX_ANCILLARY_ALARM_MILLIS = 30_000;
48
51
 
49
52
  const SubscriptionAlarmPayload = Schema.Struct({
50
53
  schemaVersion: Schema.Literal(1),
@@ -56,6 +59,105 @@ export class SubscriptionAlarmProtocolError extends Schema.TaggedError<Subscript
56
59
  { message: Schema.String },
57
60
  ) {}
58
61
 
62
+ /** Bounded host diagnostic; credentials and provider responses do not belong in alarm failures. */
63
+ export class SubscriptionAlarmExtensionError extends Schema.TaggedError<SubscriptionAlarmExtensionError>()(
64
+ "SubscriptionAlarmExtensionError",
65
+ { code: Schema.NonEmptyString.check(Schema.isMaxLength(128)) },
66
+ ) {}
67
+
68
+ /** Native partition services supplied at alarm invocation, after the host Layer is built. */
69
+ export type SubscriptionPartitionAlarmServices =
70
+ | Subscriptions
71
+ | SubscriptionIntake
72
+ | SubscriptionDriver;
73
+
74
+ export interface SubscriptionPartitionAlarmHandler<R = SubscriptionPartitionAlarmServices> {
75
+ readonly tag: string;
76
+ readonly handle: (
77
+ event: DurableObjectAlarm.DurableObjectAlarmEvent,
78
+ ) => Effect.Effect<void, SubscriptionAlarmProtocolError | SubscriptionAlarmExtensionError, R>;
79
+ }
80
+
81
+ /** Host-only handlers; the framework reserves its namespace and rejects every unknown tag. */
82
+ export const SubscriptionPartitionAlarmExtension = Context.Reference<{
83
+ readonly handlers: ReadonlyArray<SubscriptionPartitionAlarmHandler>;
84
+ }>("@effect-agent/platform-cloudflare/SubscriptionPartitionAlarmExtension", {
85
+ defaultValue: () => ({ handlers: [] }),
86
+ });
87
+
88
+ /** Capture host services once, deferring native partition services to invocation.
89
+ * Each invocation owns its codec/handler Scope and timeout.
90
+ * Callback failures stay typed. Defects and interruption reach the native alarm multiplexer.
91
+ * The host owns durable idempotency, prearming and external-effect uncertainty.
92
+ */
93
+ export const makeSubscriptionPartitionAlarmHandler = Effect.fn(
94
+ "makeSubscriptionPartitionAlarmHandler",
95
+ )(function* <Payload extends Schema.Top, R>(options: {
96
+ readonly tag: string;
97
+ readonly payload: Payload;
98
+ readonly timeoutMillis: number;
99
+ readonly handle: (
100
+ event: Omit<DurableObjectAlarm.DurableObjectAlarmEvent, "payload"> & {
101
+ readonly payload: Payload["Type"];
102
+ },
103
+ ) => Effect.Effect<void, SubscriptionAlarmExtensionError, R>;
104
+ }): Effect.fn.Return<
105
+ SubscriptionPartitionAlarmHandler<
106
+ Exclude<
107
+ Exclude<R | Payload["DecodingServices"], Scope.Scope>,
108
+ Exclude<
109
+ Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>,
110
+ SubscriptionPartitionAlarmServices
111
+ >
112
+ >
113
+ >,
114
+ SubscriptionAlarmProtocolError,
115
+ Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
116
+ > {
117
+ if (
118
+ options.tag.length === 0 ||
119
+ options.tag.length > 128 ||
120
+ options.tag.startsWith("effect-agent/") ||
121
+ !Number.isSafeInteger(options.timeoutMillis) ||
122
+ options.timeoutMillis < 1 ||
123
+ options.timeoutMillis > MAX_ANCILLARY_ALARM_MILLIS
124
+ )
125
+ return yield* SubscriptionAlarmProtocolError.make({
126
+ message: "Invalid ancillary alarm tag or timeout",
127
+ });
128
+
129
+ // Context capture includes unrequested services too; never retain a host override of native work.
130
+ const services = (yield* Effect.context<
131
+ Exclude<R | Payload["DecodingServices"], Scope.Scope | SubscriptionPartitionAlarmServices>
132
+ >()).pipe(Context.omit(Subscriptions, SubscriptionIntake, SubscriptionDriver));
133
+
134
+ return {
135
+ tag: options.tag,
136
+ handle: (event) =>
137
+ Effect.gen(function* () {
138
+ if (event.tag !== options.tag)
139
+ return yield* SubscriptionAlarmProtocolError.make({
140
+ message: "Ancillary alarm tag mismatch",
141
+ });
142
+
143
+ const payload = yield* Schema.decodeUnknownEffect(options.payload)(event.payload).pipe(
144
+ Effect.mapError(() =>
145
+ SubscriptionAlarmProtocolError.make({ message: "Invalid ancillary alarm payload" }),
146
+ ),
147
+ );
148
+
149
+ yield* options.handle({ ...event, payload });
150
+ }).pipe(
151
+ Effect.scoped,
152
+ Effect.timeout(options.timeoutMillis),
153
+ Effect.catchTag("TimeoutError", () =>
154
+ SubscriptionAlarmExtensionError.make({ code: "timeout" }),
155
+ ),
156
+ Effect.provideContext(services),
157
+ ),
158
+ };
159
+ });
160
+
59
161
  export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
60
162
  "SubscriptionPartitionProtocolError",
61
163
  { message: Schema.String.check(Schema.isMaxLength(4_096)) },
@@ -69,9 +171,11 @@ export class CloudflareSubscriptionConfigError extends Schema.TaggedError<Cloudf
69
171
  /** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
70
172
  export const validateCloudflareSubscriptionLimits = (
71
173
  limits: SubscriptionLimits,
174
+ options: { readonly ancillaryAlarms?: boolean } = {},
72
175
  ): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
73
176
  const worstCaseMillis =
74
- 4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis;
177
+ 4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
178
+ (options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
75
179
 
76
180
  return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
77
181
  ? Effect.void
@@ -96,14 +200,32 @@ const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
96
200
  parameters: PersistedJson,
97
201
  context: PersistedJson,
98
202
  mode: Schema.Literals(["once", "continuous"]),
99
- expiresAtMillis: Schema.Number,
203
+ expiresAtMillis: Schema.NullOr(Schema.Number),
100
204
  destination: SubscriptionConfiguration.fields.destination,
101
205
  deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
206
+ admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
207
+ admissionFence: SubscriptionConfiguration.fields.admissionFence,
102
208
  agentId: SubscriptionConfiguration.fields.agentId,
103
209
  definitions: SubscriptionConfiguration.fields.definitions,
104
210
  }),
105
211
  });
106
212
 
213
+ const UpdateSubscriptionRequest = Schema.TaggedStruct("UpdateSubscription", {
214
+ schemaVersion: Schema.Literal(1),
215
+ scope: SubscribeRequest.fields.scope,
216
+ key: SubscriptionKey,
217
+ expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
218
+ options: SubscribeRequest.fields.options.mapFields(({ subscriptionId: _, ...fields }) => fields),
219
+ });
220
+
221
+ const SubscriptionStateRequest = Schema.Struct({
222
+ _tag: Schema.Literals(["PauseSubscription", "ResumeSubscription", "RecoverSubscription"]),
223
+ schemaVersion: Schema.Literal(1),
224
+ scope: SubscribeRequest.fields.scope,
225
+ key: SubscriptionKey,
226
+ expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
227
+ });
228
+
107
229
  const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
108
230
  schemaVersion: Schema.Literal(1),
109
231
  scope: SubscribeRequest.fields.scope,
@@ -112,11 +234,25 @@ const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
112
234
  });
113
235
 
114
236
  const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
237
+ expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
115
238
  schemaVersion: Schema.Literal(1),
116
239
  scope: SubscribeRequest.fields.scope,
117
240
  key: SubscriptionKey,
118
241
  });
119
242
 
243
+ const GetSubscriptionRequest = Schema.TaggedStruct("GetSubscription", {
244
+ schemaVersion: Schema.Literal(1),
245
+ scope: SubscriptionScope,
246
+ key: SubscriptionKey,
247
+ });
248
+
249
+ const RecoverDeliveryRequest = Schema.TaggedStruct("RecoverDelivery", {
250
+ expectedGeneration: Schema.Natural,
251
+ schemaVersion: Schema.Literal(1),
252
+ scope: SubscribeRequest.fields.scope,
253
+ key: SubscriptionDeliveryKey,
254
+ });
255
+
120
256
  const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
121
257
  schemaVersion: Schema.Literal(1),
122
258
  scope: SubscribeRequest.fields.scope,
@@ -143,9 +279,13 @@ const StatusRequest = Schema.TaggedStruct("Status", {
143
279
 
144
280
  const SubscriptionPartitionRequest = Schema.Union([
145
281
  SubscribeRequest,
282
+ GetSubscriptionRequest,
283
+ UpdateSubscriptionRequest,
284
+ SubscriptionStateRequest,
146
285
  ListSubscriptionsRequest,
147
286
  CancelSubscriptionRequest,
148
287
  ListDeliveriesRequest,
288
+ RecoverDeliveryRequest,
149
289
  AcceptRequest,
150
290
  StatusRequest,
151
291
  ]);
@@ -182,6 +322,7 @@ const SubscriptionPartitionResponse = Schema.Union([
182
322
  Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
183
323
  Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
184
324
  Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
325
+ Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
185
326
  Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
186
327
  Schema.TaggedStruct("Status", { value: IntakeStatus }),
187
328
  Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
@@ -261,7 +402,88 @@ export class CloudflareSubscriptionsClient {
261
402
  const asProtocolError = (): SubscriptionError =>
262
403
  corruptFailure("subscription-partition-protocol");
263
404
 
405
+ const changeState = Effect.fn("CloudflareSubscriptions.changeState")(function* (
406
+ scope: typeof SubscriptionScope.Type,
407
+ key: typeof SubscriptionKey.Type,
408
+ expectedRevision: number,
409
+ _tag: "PauseSubscription" | "ResumeSubscription" | "RecoverSubscription",
410
+ ) {
411
+ const response = yield* call(scope.partition, {
412
+ _tag,
413
+ schemaVersion: 1,
414
+ scope,
415
+ key,
416
+ expectedRevision,
417
+ });
418
+
419
+ if (response._tag === "Snapshot") return response.value;
420
+ const failure = failed(response);
421
+
422
+ return yield* failure._tag === "SubscriptionError" ||
423
+ failure._tag === "SubscriptionFailpointError"
424
+ ? failure
425
+ : asProtocolError();
426
+ });
427
+
264
428
  const subscriptions = Subscriptions.of({
429
+ recoverSubscription: (scope, key, revision) =>
430
+ changeState(scope, key, revision, "RecoverSubscription"),
431
+ getSubscription: (scope, key) =>
432
+ Effect.gen(function* () {
433
+ const response = yield* call(scope.partition, {
434
+ _tag: "GetSubscription",
435
+ schemaVersion: 1,
436
+ scope,
437
+ key,
438
+ });
439
+
440
+ if (response._tag === "Snapshot") return response.value;
441
+ const failure = failed(response);
442
+
443
+ return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
444
+ }),
445
+ recoverDelivery: (scope, key, expectedGeneration) =>
446
+ Effect.gen(function* () {
447
+ const response = yield* call(scope.partition, {
448
+ _tag: "RecoverDelivery",
449
+ expectedGeneration,
450
+ schemaVersion: 1,
451
+ scope,
452
+ key,
453
+ });
454
+
455
+ if (response._tag === "DeliverySnapshot") return response.value;
456
+ const failure = failed(response);
457
+
458
+ return yield* failure._tag === "SubscriptionError" ||
459
+ failure._tag === "SubscriptionFailpointError"
460
+ ? failure
461
+ : asProtocolError();
462
+ }),
463
+ updateSubscription: (scope, key, expectedRevision, options) =>
464
+ Effect.gen(function* () {
465
+ const response = yield* call(scope.partition, {
466
+ _tag: "UpdateSubscription",
467
+ schemaVersion: 1,
468
+ scope,
469
+ key,
470
+ expectedRevision,
471
+ options,
472
+ });
473
+
474
+ if (response._tag === "Snapshot") return response.value;
475
+ const failure = failed(response);
476
+
477
+ return yield* failure._tag === "SubscriptionError" ||
478
+ failure._tag === "SubscriptionFailpointError" ||
479
+ failure._tag === "SubscriptionSourceError"
480
+ ? failure
481
+ : asProtocolError();
482
+ }),
483
+ pauseSubscription: (scope, key, revision) =>
484
+ changeState(scope, key, revision, "PauseSubscription"),
485
+ resumeSubscription: (scope, key, revision) =>
486
+ changeState(scope, key, revision, "ResumeSubscription"),
265
487
  subscribe: (scope, options) =>
266
488
  Effect.gen(function* () {
267
489
  const response = yield* call(scope.partition, {
@@ -298,10 +520,11 @@ export class CloudflareSubscriptionsClient {
298
520
 
299
521
  return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
300
522
  }),
301
- cancelSubscription: (scope, key) =>
523
+ cancelSubscription: (scope, key, expectedRevision) =>
302
524
  Effect.gen(function* () {
303
525
  const response = yield* call(scope.partition, {
304
526
  _tag: "CancelSubscription",
527
+ ...(expectedRevision === undefined ? {} : { expectedRevision }),
305
528
  schemaVersion: 1,
306
529
  scope,
307
530
  key,
@@ -461,11 +684,53 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
461
684
  SubscriptionPartitionFailure
462
685
  > {
463
686
  switch (request._tag) {
687
+ case "GetSubscription":
688
+ return {
689
+ _tag: "Snapshot",
690
+ value: yield* subscriptions.getSubscription(request.scope, request.key),
691
+ };
464
692
  case "Subscribe":
465
693
  return {
466
694
  _tag: "Snapshot",
467
695
  value: yield* subscriptions.subscribe(request.scope, request.options),
468
696
  };
697
+ case "UpdateSubscription":
698
+ return {
699
+ _tag: "Snapshot",
700
+ value: yield* subscriptions.updateSubscription(
701
+ request.scope,
702
+ request.key,
703
+ request.expectedRevision,
704
+ request.options,
705
+ ),
706
+ };
707
+ case "RecoverSubscription":
708
+ return {
709
+ _tag: "Snapshot",
710
+ value: yield* subscriptions.recoverSubscription(
711
+ request.scope,
712
+ request.key,
713
+ request.expectedRevision,
714
+ ),
715
+ };
716
+ case "PauseSubscription":
717
+ return {
718
+ _tag: "Snapshot",
719
+ value: yield* subscriptions.pauseSubscription(
720
+ request.scope,
721
+ request.key,
722
+ request.expectedRevision,
723
+ ),
724
+ };
725
+ case "ResumeSubscription":
726
+ return {
727
+ _tag: "Snapshot",
728
+ value: yield* subscriptions.resumeSubscription(
729
+ request.scope,
730
+ request.key,
731
+ request.expectedRevision,
732
+ ),
733
+ };
469
734
  case "ListSubscriptions":
470
735
  return {
471
736
  _tag: "SubscriptionPage",
@@ -478,7 +743,20 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
478
743
  case "CancelSubscription":
479
744
  return {
480
745
  _tag: "Snapshot",
481
- value: yield* subscriptions.cancelSubscription(request.scope, request.key),
746
+ value: yield* subscriptions.cancelSubscription(
747
+ request.scope,
748
+ request.key,
749
+ request.expectedRevision,
750
+ ),
751
+ };
752
+ case "RecoverDelivery":
753
+ return {
754
+ _tag: "DeliverySnapshot",
755
+ value: yield* subscriptions.recoverDelivery(
756
+ request.scope,
757
+ request.key,
758
+ request.expectedGeneration,
759
+ ),
482
760
  };
483
761
  case "ListDeliveries":
484
762
  return {
@@ -566,7 +844,29 @@ const alarmHandler = (limits: SubscriptionLimits) =>
566
844
  DurableObjectAlarm.processDue(
567
845
  (event) =>
568
846
  Effect.gen(function* () {
569
- if (event.tag !== SUBSCRIPTION_ALARM_TAG || event.id !== SUBSCRIPTION_ALARM_ID) {
847
+ if (event.tag !== SUBSCRIPTION_ALARM_TAG) {
848
+ const { handlers } = yield* SubscriptionPartitionAlarmExtension;
849
+ const matches = handlers.filter((handler) => handler.tag === event.tag);
850
+ const handler = matches[0];
851
+
852
+ if (
853
+ event.tag.startsWith("effect-agent/") ||
854
+ matches.length !== 1 ||
855
+ handler === undefined
856
+ )
857
+ return yield* SubscriptionAlarmProtocolError.make({
858
+ message: "Unknown or ambiguous ancillary alarm tag",
859
+ });
860
+
861
+ return yield* handler.handle(event).pipe(
862
+ Effect.scoped,
863
+ Effect.timeout(MAX_ANCILLARY_ALARM_MILLIS),
864
+ Effect.catchTag("TimeoutError", () =>
865
+ SubscriptionAlarmExtensionError.make({ code: "timeout" }),
866
+ ),
867
+ );
868
+ }
869
+ if (event.id !== SUBSCRIPTION_ALARM_ID) {
570
870
  return yield* SubscriptionAlarmProtocolError.make({
571
871
  message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
572
872
  });
@@ -600,7 +900,12 @@ const alarmHandler = (limits: SubscriptionLimits) =>
600
900
  yield* alarmControl.reconcile;
601
901
  }
602
902
  }),
603
- { mode: "ordered" },
903
+ {
904
+ mode: "isolated",
905
+ limit: MAX_ALARMS_PER_INVOCATION,
906
+ retryFailedAfter: limits.retryMillis,
907
+ onFailure: () => Effect.logWarning("Subscription partition alarm retained for retry"),
908
+ },
604
909
  ).pipe(Effect.asVoid);
605
910
 
606
911
  type SubscriptionRuntimeServices =
@@ -669,6 +974,29 @@ export const makeSubscriptionPartitionObjectClass = <E>(
669
974
  SubscriptionIntake.layer(limits),
670
975
  SubscriptionDriver.layer(limits),
671
976
  cloudflareLimitsLayer,
977
+ // Capture the host reference before its scoped input Layer is hidden from the runtime.
978
+ Layer.effect(
979
+ SubscriptionPartitionAlarmExtension,
980
+ Effect.gen(function* () {
981
+ const extension = yield* SubscriptionPartitionAlarmExtension;
982
+ const tags = extension.handlers.map((handler) => handler.tag);
983
+
984
+ if (
985
+ tags.length > MAX_ALARMS_PER_INVOCATION ||
986
+ new Set(tags).size !== tags.length ||
987
+ tags.some(
988
+ (tag) => tag.length === 0 || tag.length > 128 || tag.startsWith("effect-agent/"),
989
+ )
990
+ )
991
+ return yield* SubscriptionAlarmProtocolError.make({
992
+ message: "Invalid ancillary alarm handler registry",
993
+ });
994
+
995
+ yield* validateCloudflareSubscriptionLimits(limits, { ancillaryAlarms: tags.length > 0 });
996
+
997
+ return extension;
998
+ }),
999
+ ),
672
1000
  ).pipe(
673
1001
  Layer.provideMerge(partitionStore),
674
1002
  Layer.provide(
@@ -682,7 +1010,11 @@ export const makeSubscriptionPartitionObjectClass = <E>(
682
1010
 
683
1011
  const runtime: Layer.Layer<
684
1012
  SubscriptionRuntimeServices,
685
- E | SubscriptionError | SubscriptionPartitionProtocolError | CloudflareSubscriptionConfigError,
1013
+ | E
1014
+ | SubscriptionError
1015
+ | SubscriptionPartitionProtocolError
1016
+ | CloudflareSubscriptionConfigError
1017
+ | SubscriptionAlarmProtocolError,
686
1018
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
687
1019
  > = Layer.effectContext(
688
1020
  Effect.gen(function* () {
@@ -15,9 +15,12 @@ import {
15
15
  PersistedJson,
16
16
  } from "@effect-agent/thread/Records";
17
17
  import {
18
+ AdmissionFence,
19
+ AdmissionGroup,
18
20
  AbortCommand,
19
21
  AbortIntent,
20
22
  AdmissionConflict,
23
+ AdmissionPolicyError,
21
24
  ApprovalConflict,
22
25
  ApprovalDecisionCommand,
23
26
  ApprovalDecisionIntent,
@@ -31,6 +34,7 @@ import {
31
34
  UnknownResolutionConflict,
32
35
  UnknownResolutionIntent,
33
36
  } from "@effect-agent/thread/SubmissionLedger";
37
+ import { SubmissionStatus } from "@effect-agent/thread/SubmissionStatus";
34
38
  import {
35
39
  AppendConflict,
36
40
  ThreadNotMaterialized,
@@ -107,6 +111,8 @@ export class SubmitRequest extends Schema.Class<SubmitRequest>(
107
111
  agentId: AgentId,
108
112
  principal: Principal,
109
113
  idempotencyKey: IdempotencyKey,
114
+ admissionGroup: Schema.optionalKey(AdmissionGroup),
115
+ admissionFence: Schema.optionalKey(AdmissionFence),
110
116
  definitions: DefinitionDigests,
111
117
  inputPayload: PersistedJson,
112
118
  }) {}
@@ -148,6 +154,7 @@ export const HostFailure = Schema.Union([
148
154
  AgentInputError,
149
155
  DigestError,
150
156
  AdmissionConflict,
157
+ AdmissionPolicyError,
151
158
  SettlementConflict,
152
159
  ApprovalConflict,
153
160
  UnknownResolutionConflict,
@@ -172,6 +179,10 @@ export class SubmitSucceeded extends Schema.TaggedClass<SubmitSucceeded>(
172
179
  receipt: Receipt,
173
180
  }) {}
174
181
 
182
+ export class SubmissionStatusResponse extends Schema.TaggedClass<SubmissionStatusResponse>(
183
+ "@effect-agent/platform-cloudflare/SubmissionStatusResponse",
184
+ )("SubmissionStatusResponse", { status: SubmissionStatus }) {}
185
+
175
186
  export class SettlementReached extends Schema.TaggedClass<SettlementReached>(
176
187
  "@effect-agent/platform-cloudflare/SettlementReached",
177
188
  )("SettlementReached", {
@@ -222,6 +233,7 @@ export class HostFailed extends Schema.TaggedClass<HostFailed>(
222
233
  export const HostResponse = Schema.Union([
223
234
  SubmitSucceeded,
224
235
  SettlementReached,
236
+ SubmissionStatusResponse,
225
237
  ObservedPage,
226
238
  ProgressObserved,
227
239
  ProgressCancelled,
@@ -265,6 +277,7 @@ const ClientSubmitHostFailure = Schema.Union([
265
277
  AgentInputError,
266
278
  DigestError,
267
279
  AdmissionConflict,
280
+ AdmissionPolicyError,
268
281
  LedgerError,
269
282
  ThreadStoreError,
270
283
  ThreadNotMaterialized,
@@ -339,6 +352,7 @@ const outOfContract = (threadId: string, operation: string, observed: string): T
339
352
  const hostRpcMethods = {
340
353
  submit: "submitEncoded",
341
354
  awaitSettlement: "awaitSettlementEncoded",
355
+ submissionStatus: "submissionStatusEncoded",
342
356
  awaitProgress: "awaitProgressEncoded",
343
357
  cancelProgress: "cancelProgressEncoded",
344
358
  observePage: "observePage",
@@ -358,6 +372,9 @@ export class CloudflareThreadClient extends Context.Service<
358
372
  options: DurableSubmitOptions,
359
373
  ) => Effect.Effect<Receipt, ClientSubmitFailure, InputSchema["EncodingServices"]>;
360
374
  /** Wake-hinted, poll-guaranteed settlement wait executed inside the owning Object. */
375
+ readonly submissionStatus: (
376
+ receipt: Receipt,
377
+ ) => Effect.Effect<SubmissionStatus, ClientAwaitFailure>;
361
378
  readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, ClientAwaitFailure>;
362
379
  /**
363
380
  * Wait without polling until progress after `afterSequence` is already durable or hinted.
@@ -572,6 +589,12 @@ export class CloudflareThreadClient extends Context.Service<
572
589
  agentId: agent.definition.id,
573
590
  principal: options.principal,
574
591
  idempotencyKey: options.idempotencyKey,
592
+ ...(options.admissionGroup === undefined
593
+ ? {}
594
+ : { admissionGroup: options.admissionGroup }),
595
+ ...(options.admissionFence === undefined
596
+ ? {}
597
+ : { admissionFence: options.admissionFence }),
575
598
  definitions: options.definitions,
576
599
  inputPayload,
577
600
  });
@@ -596,6 +619,23 @@ export class CloudflareThreadClient extends Context.Service<
596
619
  return succeeded.receipt;
597
620
  }),
598
621
 
622
+ submissionStatus: (receipt) =>
623
+ Effect.gen(function* () {
624
+ const encoded = yield* encodeReceipt(receipt).pipe(
625
+ Effect.mapError(() => HostProtocolError.make({ message: "Invalid receipt" })),
626
+ );
627
+
628
+ const response = yield* call(receipt.threadId, "submissionStatus", encoded);
629
+
630
+ const result = yield* expect(
631
+ receipt.threadId,
632
+ "submissionStatus",
633
+ SubmissionStatusResponse,
634
+ ClientAwaitHostFailure,
635
+ )(response);
636
+
637
+ return result.status;
638
+ }),
599
639
  awaitSettlement: (receipt) =>
600
640
  Effect.gen(function* () {
601
641
  const encoded = yield* encodeReceipt(receipt).pipe(
@@ -28,6 +28,7 @@ import {
28
28
  import { PersistedJson } from "@effect-agent/thread/Records";
29
29
  import { RunJournalError } from "@effect-agent/thread/RunJournal";
30
30
  import {
31
+ AdmissionPolicyError,
31
32
  LedgerError,
32
33
  OwnershipLost,
33
34
  SettlementConflict,
@@ -73,6 +74,7 @@ import {
73
74
  ProgressObserved,
74
75
  ProgressCancelled,
75
76
  SettlementReached,
77
+ SubmissionStatusResponse,
76
78
  SubmitSucceeded,
77
79
  UnknownResolutionRecorded,
78
80
  boundHostDiagnostic,
@@ -308,6 +310,12 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
308
310
  threadId: identity.threadId,
309
311
  principal: request.principal,
310
312
  idempotencyKey: request.idempotencyKey,
313
+ ...(request.admissionGroup === undefined
314
+ ? {}
315
+ : { admissionGroup: request.admissionGroup }),
316
+ ...(request.admissionFence === undefined
317
+ ? {}
318
+ : { admissionFence: request.admissionFence }),
311
319
  definitions: request.definitions,
312
320
  }),
313
321
  );
@@ -319,6 +327,22 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
319
327
  Effect.flatMap(encodeResponse),
320
328
  );
321
329
 
330
+ const submissionStatusEndpoint = (
331
+ encoded: unknown,
332
+ ): Effect.Effect<unknown, never, EndpointServices> =>
333
+ decodeReceipt(encoded).pipe(
334
+ Effect.mapError(protocolFailure("The receipt could not be decoded")),
335
+ Effect.flatMap((receipt) =>
336
+ Effect.gen(function* () {
337
+ const runtime = yield* DurableAgentRuntime;
338
+
339
+ return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
340
+ }),
341
+ ),
342
+ respond,
343
+ Effect.flatMap(encodeResponse),
344
+ );
345
+
322
346
  const awaitSettlementEndpoint = (
323
347
  encoded: unknown,
324
348
  ): Effect.Effect<unknown, never, EndpointServices> =>
@@ -492,6 +516,7 @@ export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
492
516
 
493
517
  /** Every typed failure of the four admin entry points, plus the protocol's own errors. */
494
518
  export const AdminFailure = Schema.Union([
519
+ AdmissionPolicyError,
495
520
  OperationDenied,
496
521
  RetryRefused,
497
522
  LedgerError,
@@ -781,6 +806,7 @@ export interface Instance<EventServices = never> extends InstanceType<
781
806
  EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
782
807
  > {
783
808
  submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
809
+ submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
784
810
  awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
785
811
  awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
786
812
  cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
@@ -859,6 +885,7 @@ export const make = <
859
885
 
860
886
  const rpc = {
861
887
  submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
888
+ submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
862
889
  awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
863
890
  awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
864
891
  cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),