@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,16 +1,16 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, threadNamespaceFromEnv } from "./CloudflareBindings.mjs";
2
+ import { DurableObjectContext, ThreadObjectIdentity, ThreadObjectNamespace, ThreadObjectPlacement, threadNamespaceFromEnv } from "./CloudflareBindings.mjs";
3
3
  import { AdmissionLimitExceeded, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError } from "./CloudflareConfig.mjs";
4
4
  import { DurableAlarmError, DurableAlarmService, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, publishCommitted } from "./Alarm.mjs";
5
5
  import { AbortRecorded, ApprovalRecorded, CloudflareThreadClient, HostFailed, HostProtocolError, ObservedPage, ProgressCancelled, ProgressObserved, SettlementReached, SubmissionStatusResponse, SubmitSucceeded, UnknownResolutionRecorded, boundHostDiagnostic, decodeAbortCommand, decodeApprovalDecisionCommand, decodeAwaitProgressRequest, decodeCancelProgressRequest, decodeObservePageRequest, decodeReceipt, decodeSubmitRequest, decodeUnknownResolutionCommand, encodeHostResponse } from "./CloudflareThreadClient.mjs";
6
- import { t as cloudflarePreparedInputAdmissionLayer } from "./prepared-admission-G7N4DDqS.mjs";
6
+ import { t as cloudflarePreparedInputAdmissionLayer } from "./prepared-admission-DSi55W-7.mjs";
7
7
  import { cloudflareWakeSchedulerLayer } from "./WakeScheduler.mjs";
8
+ import { SubmissionId, ThreadId } from "@effect-agent/core/Identifiers";
8
9
  import { compileRegistrations } from "@effect-agent/thread/AgentRegistration";
9
10
  import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryReport } from "@effect-agent/thread/DurableAgentRuntime";
10
- import { AdmissionPolicyError, LedgerError, OwnershipLost, SettlementConflict, SubmissionLedger, SubmissionLookupByKey } from "@effect-agent/thread/SubmissionLedger";
11
+ import { AdmissionPolicyError, LedgerError, OwnershipLost, SettlementConflict, SubmissionLedger, SubmissionLookupById, SubmissionLookupByKey } from "@effect-agent/thread/SubmissionLedger";
11
12
  import { ThreadProjectionMaintenance } from "@effect-agent/thread/ThreadProjectionMaintenance";
12
13
  import { Cause, Clock, Context, Deferred, Duration, Effect, Layer, Option, Ref, Schema, Semaphore, Stream } from "effect";
13
- import { SubmissionId, ThreadId } from "@effect-agent/core/Identifiers";
14
14
  import { PersistedJson, ProducerId } from "@effect-agent/thread/Records";
15
15
  import { DurableObject, DurableObjectState, WorkerEnvironment } from "effect-cf";
16
16
  import { BrowserCrypto } from "@effect/platform-browser";
@@ -19,7 +19,8 @@ import { DigestError } from "@effect-agent/thread/Digest";
19
19
  import { DurableRuntimeFailpoint, DurableRuntimeFailpointError } from "@effect-agent/thread/DurableFailpoint";
20
20
  import { OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, operationAuthorizerLayer } from "@effect-agent/thread/OperationAuthorizer";
21
21
  import { AppendConflict, FenceRejected, ThreadNotMaterialized, ThreadRead, ThreadStore, ThreadStoreError } from "@effect-agent/thread/ThreadStore";
22
- import { decodePortRequest, encodePortResponse } from "@effect-agent/storage-cloudflare/PortProtocol";
22
+ import "@effect-agent/thread/PreparedInputAdmission";
23
+ import { LedgerLookupResult, PortFailed, PortProtocolError, PortSucceeded, decodePortRequest, encodePortResponse } from "@effect-agent/storage-cloudflare/PortProtocol";
23
24
  import { IntegrityReport, ObligationReport, ObligationThresholds, RecoveryExplanation, RetryCommand, RetryRefused } from "@effect-agent/thread/Admin";
24
25
  import { RunJournalError } from "@effect-agent/thread/RunJournal";
25
26
  import { WakeScheduler } from "@effect-agent/thread/WakeScheduler";
@@ -38,10 +39,10 @@ const guardedMessageDeliveryStoreLayer = Layer.effect(MessageDeliveryStore, Effe
38
39
  const store = yield* MessageDeliveryStore;
39
40
  const mutations = yield* ThreadMutationGate;
40
41
  const wakes = yield* WakeScheduler;
41
- const { threadId } = yield* ThreadObjectIdentity;
42
+ const { ownsThread } = yield* ThreadObjectPlacement;
42
43
  const deadline = yield* Ref.make(void 0);
43
44
  const cacheGate = yield* Semaphore.make(1);
44
- const local = (owner, body) => owner === threadId ? body : Effect.fail(MessageDeliveryError.make({
45
+ const local = (owner, body) => owner === void 0 || ownsThread(owner) ? body : Effect.fail(MessageDeliveryError.make({
45
46
  reason: "validation",
46
47
  operation: "message owner"
47
48
  }));
@@ -52,17 +53,20 @@ const guardedMessageDeliveryStoreLayer = Layer.effect(MessageDeliveryStore, Effe
52
53
  const nextDeadline = cacheGate.withPermit(Effect.gen(function* () {
53
54
  const cached = yield* Ref.get(deadline);
54
55
  if (cached !== void 0) return cached;
55
- const current = yield* store.nextDeadline(threadId);
56
+ const current = yield* store.nextDeadline();
56
57
  yield* Ref.set(deadline, current);
57
58
  return current;
58
59
  }));
59
60
  return MessageDeliveryStore.of({
60
- insert: (record) => local(record.key.ownerThreadId, mutate(store.insert(record)).pipe(Effect.tap(() => wakes.notify(threadId)))),
61
+ insert: (record) => local(record.key.ownerThreadId, mutate(store.insert(record)).pipe(Effect.tap(() => wakes.notify(record.key.ownerThreadId)))),
61
62
  get: (key) => local(key.ownerThreadId, store.get(key)),
62
63
  list: (request) => local(request.ownerThreadId, store.list(request)),
63
64
  change: (key, change) => local(key.ownerThreadId, mutate(store.change(key, change))),
64
- due: (nowMillis, limit, owner = threadId) => local(owner, store.due(nowMillis, limit, threadId)),
65
- nextDeadline: (owner = threadId) => local(owner, nextDeadline)
65
+ due: (nowMillis, limit, owner) => local(owner, store.due(nowMillis, limit, owner)).pipe(Effect.flatMap((keys) => keys.every((key) => ownsThread(key.ownerThreadId)) ? Effect.succeed(keys) : Effect.fail(MessageDeliveryError.make({
66
+ reason: "validation",
67
+ operation: "message owner"
68
+ })))),
69
+ nextDeadline: (owner) => local(owner, owner === void 0 ? nextDeadline : store.nextDeadline(owner))
66
70
  });
67
71
  }));
68
72
  /** Message progress shares the native alarm slot without blocking source runtime work. */
@@ -71,27 +75,26 @@ const threadMessageDeliveryLayer = Layer.effectContext(Effect.gen(function* () {
71
75
  const store = yield* MessageDeliveryStore;
72
76
  const wakes = yield* WakeScheduler;
73
77
  const config = yield* CloudflareDurableRuntimeConfig;
74
- const { threadId } = yield* ThreadObjectIdentity;
75
78
  const failure = (operation) => () => DurableAlarmError.make({
76
79
  operation,
77
80
  message: "Durable message recovery remains pending"
78
81
  });
79
82
  const drain = Effect.gen(function* () {
80
- const deadline = yield* store.nextDeadline(threadId);
81
- if (deadline !== null && deadline <= (yield* Clock.currentTimeMillis)) yield* driver.runDue(threadId);
83
+ const deadline = yield* store.nextDeadline();
84
+ if (deadline !== null && deadline <= (yield* Clock.currentTimeMillis)) yield* driver.runDue();
82
85
  }).pipe(Effect.mapError(failure("drain message delivery")));
83
86
  return Context.make(ThreadMessageDelivery, {
84
87
  drain,
85
88
  drainUntil: Effect.fn("ThreadMessageDelivery.drainUntil")(function* (finished) {
89
+ const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.catch(() => Effect.never));
86
90
  yield* Effect.gen(function* () {
87
- const notified = yield* wakes.subscribe(threadId);
88
91
  yield* drain;
89
- const deadline = yield* store.nextDeadline(threadId).pipe(Effect.mapError(failure("read message deadline")));
92
+ const deadline = yield* store.nextDeadline().pipe(Effect.mapError(failure("read message deadline")));
90
93
  const delay = deadline === null ? config.wakeScanInterval : Math.min(config.wakeScanInterval, Math.max(1, deadline - (yield* Clock.currentTimeMillis)));
91
94
  yield* Effect.raceFirst(Deferred.await(finished), Effect.raceFirst(notified, Effect.sleep(delay)));
92
- }).pipe(Effect.scoped, Effect.repeat({ until: () => Deferred.isDone(finished) }));
93
- }),
94
- pendingDeadline: store.nextDeadline(threadId).pipe(Effect.map(Option.fromNullishOr), Effect.mapError(failure("read message deadline")))
95
+ }).pipe(Effect.repeat({ until: () => Deferred.isDone(finished) }));
96
+ }, Effect.scoped),
97
+ pendingDeadline: store.nextDeadline().pipe(Effect.map(Option.fromNullishOr), Effect.mapError(failure("read message deadline")))
95
98
  });
96
99
  }));
97
100
  //#endregion
@@ -172,9 +175,9 @@ var ProgressWaitRegistry = class ProgressWaitRegistry extends Context.Service()(
172
175
  * turns exactly this error into `AdmissionIndeterminate` (SUB-031).
173
176
  */
174
177
  const threadPortTransportLayer = Layer.effect(ThreadPortTransport)(Effect.gen(function* () {
175
- const { namespace } = yield* ThreadObjectNamespace;
178
+ const { get } = yield* ThreadObjectNamespace;
176
179
  return ThreadPortTransport.of({ call: (threadId, request) => Effect.tryPromise({
177
- try: () => namespace.get(namespace.idFromName(threadId)).portCall(request),
180
+ try: () => get(threadId).portCall(request),
178
181
  catch: (cause) => portTransportFailure(threadId, cause)
179
182
  }).pipe(Effect.withSpan("CloudflarePortTransport.call", { attributes: { threadId } })) });
180
183
  }));
@@ -226,18 +229,10 @@ const threadIdFromState = (ctx) => ctx.id.name === void 0 ? Effect.fail(Cloudfla
226
229
  * The native class factory builds this Layer inside its constructor gate. Custom Effect hosts
227
230
  * can provide it around the complete application Layer with the native context already supplied.
228
231
  */
229
- const layerConfig = (options) => Layer.unwrap(Effect.gen(function* () {
232
+ const runtimeConfigLayer = (options, producerId) => Layer.unwrap(Effect.gen(function* () {
230
233
  const { ctx } = yield* DurableObjectContext;
231
234
  const config = yield* configFromOptions(options);
232
- const threadId = yield* threadIdFromState(ctx);
233
- const producerId = yield* decodeProducerId(`${config.producerPrefix}:${threadId}`).pipe(Effect.mapError((error) => CloudflarePlatformConfigError.make({
234
- message: `The minted producer identity is invalid: ${error.message}`,
235
- cause: error
236
- })));
237
- return Layer.mergeAll(Layer.succeed(CloudflareDurableRuntimeConfig, config), Layer.succeed(ThreadObjectIdentity, {
238
- threadId,
239
- producerId
240
- }), DurableRuntimeConfig.layer({
235
+ return Layer.mergeAll(Layer.succeed(CloudflareDurableRuntimeConfig, config), DurableRuntimeConfig.layer({
241
236
  deploymentId: config.deploymentId,
242
237
  producerId,
243
238
  settlementPollInterval: Duration.millis(config.settlementPollInterval),
@@ -249,6 +244,30 @@ const layerConfig = (options) => Layer.unwrap(Effect.gen(function* () {
249
244
  failpoint: options.storageFailpoint?.(ctx)
250
245
  }), options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint, { hit: options.runtimeFailpoint(ctx) }), options.maintenanceFailpoint === void 0 ? ThreadMaintenanceFailpoint.layer : Layer.succeed(ThreadMaintenanceFailpoint, { hit: options.maintenanceFailpoint(ctx) }), options.toolReconciler ?? ToolReconciler.uncertain, options.operationAuthorizer === void 0 ? Layer.empty : operationAuthorizerLayer(options.operationAuthorizer), options.toolFailureObserver === void 0 ? Layer.succeed(CurrentToolFailureObserver, void 0) : toolFailureObserverLayer(options.toolFailureObserver), RunContextPreparationPassthrough, RunToolAuthorization.allowAll);
251
246
  }));
247
+ const producerIdentity = (prefix, owner) => decodeProducerId(`${prefix}:${owner}`).pipe(Effect.mapError((error) => CloudflarePlatformConfigError.make({
248
+ message: `The minted producer identity is invalid: ${error.message}`,
249
+ cause: error
250
+ })));
251
+ /** Native one-Thread configuration; retains its existing producer and logical identities. */
252
+ const layerConfig = (options) => Layer.unwrap(Effect.gen(function* () {
253
+ const { ctx } = yield* DurableObjectContext;
254
+ const threadId = yield* threadIdFromState(ctx);
255
+ const producerId = yield* producerIdentity(options.producerPrefix, threadId);
256
+ return Layer.mergeAll(runtimeConfigLayer(options, producerId), Layer.succeed(ThreadObjectIdentity, {
257
+ threadId,
258
+ producerId
259
+ }), Layer.succeed(ThreadObjectPlacement, { ownsThread: (target) => target === threadId }));
260
+ }));
261
+ /**
262
+ * Configuration for an application Object owning several logical Threads. The producer is
263
+ * the stable physical Object. No logical identity is installed globally: handleRpc binds and
264
+ * validates it per request using the placement guard and this actual runtime producer.
265
+ */
266
+ const layerHostConfig = (options, ownsThread) => Layer.unwrap(Effect.gen(function* () {
267
+ const { ctx } = yield* DurableObjectContext;
268
+ const producerId = yield* producerIdentity(options.producerPrefix, ctx.id.toString());
269
+ return Layer.merge(runtimeConfigLayer(options, producerId), Layer.succeed(ThreadObjectPlacement, { ownsThread }));
270
+ }));
252
271
  /**
253
272
  * Register typed Agents and version declarations. Hashing and dependency capture happen in
254
273
  * this Layer's Scope, after application Layers have been provided. Every Agent's instruction,
@@ -265,10 +284,14 @@ function layer(registrations, options = {}) {
265
284
  * Supply host services through `ThreadObject.make` or `ThreadObject.layerConfig` and
266
285
  * the Durable Object context and namespace Layers when composing a custom host.
267
286
  */
268
- const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.gen(function* () {
287
+ const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.map(DurableObjectContext, ({ ctx }) => sharedLayer(DurableAgentRuntime.layerWithBindings(bindings), options).pipe(Layer.provideMerge(SqliteClient.layer({ storage: ctx.storage })))));
288
+ function layerInHost(application, options = {}) {
289
+ return sharedLayer(application, options);
290
+ }
291
+ const sharedLayer = (application, options = {}) => Layer.unwrap(Effect.gen(function* () {
269
292
  const { ctx } = yield* DurableObjectContext;
270
293
  const config = yield* CloudflareDurableRuntimeConfig;
271
- const { threadId } = yield* ThreadObjectIdentity;
294
+ const { ownsThread } = yield* ThreadObjectPlacement;
272
295
  const storageOptions = {
273
296
  storage: ctx.storage,
274
297
  observationPollInterval: config.observationPollInterval,
@@ -276,7 +299,7 @@ const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.gen(function*
276
299
  maxStoredValueBytes: config.maxStoredValueBytes,
277
300
  verifyOnOpen: config.verifyOnOpen
278
301
  };
279
- const infrastructure = Layer.mergeAll(storageConfigLayer(storageOptions), SqliteClient.layer({ storage: ctx.storage }));
302
+ const infrastructure = Layer.mergeAll(storageConfigLayer(storageOptions), Layer.effect(SqlClient)(SqlClient));
280
303
  const rawLocalPorts = Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(Layer.provide(infrastructure));
281
304
  const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
282
305
  const wakes = cloudflareWakeSchedulerLayer.pipe(Layer.provide(base));
@@ -317,10 +340,14 @@ const boundLayer = (bindings, options = {}) => Layer.unwrap(Effect.gen(function*
317
340
  })).pipe(Layer.provide(rawLocalPorts));
318
341
  const portsEndpointLayer = Layer.effect(ThreadObjectPorts)(Effect.gen(function* () {
319
342
  const local = yield* Effect.context();
320
- return ThreadObjectPorts.of({ handle: (request) => executePortRequest(request).pipe(Effect.provide(local)) });
343
+ const ledger = Context.get(local, SubmissionLedger);
344
+ return ThreadObjectPorts.of({
345
+ handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
346
+ lookupSubmission: (submissionId) => ledger.lookup(SubmissionLookupById.make({ submissionId }))
347
+ });
321
348
  })).pipe(Layer.provide(localPorts));
322
- const routedPorts = Layer.mergeAll(routedSubmissionLedgerLayer({ localThreadId: threadId }), routedThreadStoreLayer({ localThreadId: threadId })).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
323
- const runtimeStack = DurableAgentRuntime.layerWithBindings(bindings).pipe(Layer.provide(cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer))), Layer.provideMerge(messageStore), Layer.provideMerge(routedPorts), Layer.provideMerge(wakes), Layer.provideMerge(base));
349
+ const routedPorts = Layer.mergeAll(routedSubmissionLedgerLayer({ ownsThread }), routedThreadStoreLayer({ ownsThread })).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
350
+ const runtimeStack = application.pipe(Layer.provide(cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer))), Layer.provideMerge(messageStore), Layer.provideMerge(routedPorts), Layer.provideMerge(wakes), Layer.provideMerge(base), Layer.provideMerge(portsEndpointLayer));
324
351
  return Layer.mergeAll(runtimeStack, ThreadMaintenance.layer.pipe(Layer.provide(runtimeStack), Layer.provide(messageRecovery)), portsEndpointLayer, messageStore, messageRecovery).pipe(Layer.provideMerge(publication), Layer.provideMerge(projection), Layer.provideMerge(ThreadMutationGate.layer), Layer.provideMerge(infrastructure));
325
352
  }));
326
353
  //#endregion
@@ -334,6 +361,8 @@ var ThreadObject_exports = /* @__PURE__ */ __exportAll({
334
361
  ExplainedRecovery: () => ExplainedRecovery,
335
362
  ObligationsScanned: () => ObligationsScanned,
336
363
  RetryExecuted: () => RetryExecuted,
364
+ ThreadObjectPorts: () => ThreadObjectPorts,
365
+ ThreadRpcOperation: () => ThreadRpcOperation,
337
366
  VerifiedIntegrity: () => VerifiedIntegrity,
338
367
  decodeAdminExplainRequest: () => decodeAdminExplainRequest,
339
368
  decodeAdminResponse: () => decodeAdminResponse,
@@ -341,9 +370,14 @@ var ThreadObject_exports = /* @__PURE__ */ __exportAll({
341
370
  decodeObligationThresholds: () => decodeObligationThresholds,
342
371
  decodeRetryCommand: () => decodeRetryCommand,
343
372
  encodeAdminResponse: () => encodeAdminResponse,
373
+ handleRpc: () => handleRpc,
344
374
  layer: () => layer,
345
375
  layerConfig: () => layerConfig,
346
- make: () => make
376
+ layerHostConfig: () => layerHostConfig,
377
+ layerInHost: () => layerInHost,
378
+ make: () => make,
379
+ portCall: () => portCall,
380
+ submit: () => submit
347
381
  });
348
382
  /** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
349
383
  const isMutatingPortRequest = (request) => {
@@ -388,13 +422,12 @@ const utf8Bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).len
388
422
  * exempt: its accepted-work obligation already exists, and returning the original Receipt
389
423
  * consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
390
424
  */
391
- const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request) {
392
- const identity = yield* ThreadObjectIdentity;
425
+ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (threadId, request) {
393
426
  const config = yield* CloudflareDurableRuntimeConfig;
394
427
  const ledger = yield* SubmissionLedger;
395
428
  const { ctx } = yield* DurableObjectContext;
396
429
  const existing = yield* ledger.lookup(SubmissionLookupByKey.make({
397
- threadId: identity.threadId,
430
+ threadId,
398
431
  principal: request.principal,
399
432
  idempotencyKey: request.idempotencyKey
400
433
  }));
@@ -405,7 +438,7 @@ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(functi
405
438
  actual: inputBytes,
406
439
  maximum: config.limits.maxInputBytes
407
440
  });
408
- const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
441
+ const nonterminal = yield* ledger.scanNonterminal.pipe(Stream.filter((submission) => submission.threadId === threadId), Stream.runCollect);
409
442
  if (nonterminal.length >= config.limits.maxQueueDepthPerLane) return yield* AdmissionLimitExceeded.make({
410
443
  limit: "queue-depth",
411
444
  actual: nonterminal.length,
@@ -428,13 +461,51 @@ const passthroughSubmitAgent = (agentId) => ({ definition: {
428
461
  id: agentId,
429
462
  input: PersistedJson
430
463
  } });
431
- const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.mapError(protocolFailure("The submit request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
432
- const identity = yield* ThreadObjectIdentity;
433
- const maintenance = yield* ThreadMaintenance;
464
+ /** A physical owner may hold other Threads; an addressed request cannot act on their IDs. */
465
+ const lookupAddressedSubmission = Effect.fn("ThreadObject.lookupAddressedSubmission")(function* (submissionId) {
466
+ const { threadId } = yield* ThreadObjectIdentity;
467
+ const submission = yield* (yield* ThreadObjectPorts).lookupSubmission(submissionId);
468
+ if (Option.isSome(submission) && submission.value.threadId !== threadId) return yield* HostProtocolError.make({ message: "The Submission belongs to another Thread" });
469
+ return submission;
470
+ });
471
+ const requireSubmissionThread = Effect.fn("ThreadObject.requireSubmissionThread")(function* (submissionId) {
472
+ const submission = yield* lookupAddressedSubmission(submissionId);
473
+ if (Option.isNone(submission)) return yield* LedgerError.make({
474
+ operation: "addressed Submission lookup",
475
+ message: "The addressed Thread has no such Submission"
476
+ });
477
+ });
478
+ const requireReceiptThread = Effect.fn("ThreadObject.requireReceiptThread")(function* (threadId) {
479
+ if (threadId !== (yield* ThreadObjectIdentity).threadId) return yield* HostProtocolError.make({ message: "The Receipt belongs to another Thread" });
480
+ });
481
+ const requirePortThread = (request) => {
482
+ switch (request._tag) {
483
+ case "LedgerLookup": return request.request._tag === "SubmissionLookupById" ? lookupAddressedSubmission(request.request.submissionId).pipe(Effect.asVoid) : requireReceiptThread(request.request.threadId);
484
+ case "LedgerMarkReady":
485
+ case "LedgerRequestAbort": return requireSubmissionThread(request.request.submissionId);
486
+ case "LedgerRecordChildSettled": return requireSubmissionThread(request.request.parentSubmissionId);
487
+ case "LedgerAdmit":
488
+ case "LedgerResolveAdmission":
489
+ case "StoreMaterialize":
490
+ case "StoreAppend":
491
+ case "StoreReadPage":
492
+ case "StoreInspectTail":
493
+ case "StoreExport": return requireReceiptThread(request.request.threadId);
494
+ }
495
+ };
496
+ /**
497
+ * Admit an already Schema-decoded request to a logical Thread in this physical owner.
498
+ * Custom hosts validate local placement before calling this Effect and provide their same
499
+ * runtime/maintenance instances. The native endpoint uses this path too: queue limits,
500
+ * idempotent receipts and the pre-admission generation/alarm commit have one owner.
501
+ */
502
+ const submit = Effect.fn("ThreadObject.submit")(function* (threadId, request) {
503
+ if (!(yield* ThreadObjectPlacement).ownsThread(threadId)) return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
504
+ const mutations = yield* ThreadMutationGate;
434
505
  const runtime = yield* DurableAgentRuntime;
435
- yield* gateAdmissionLimits(request);
436
- const receipt = yield* maintenance.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
437
- threadId: identity.threadId,
506
+ yield* gateAdmissionLimits(threadId, request);
507
+ return yield* mutations.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
508
+ threadId,
438
509
  principal: request.principal,
439
510
  idempotencyKey: request.idempotencyKey,
440
511
  ...request.admissionGroup === void 0 ? {} : { admissionGroup: request.admissionGroup },
@@ -442,14 +513,32 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
442
513
  ...request.workerAdmission === void 0 ? {} : { workerAdmission: request.workerAdmission },
443
514
  ...request.messageAdmission === void 0 ? {} : { messageAdmission: request.messageAdmission },
444
515
  definitions: request.definitions
445
- }));
516
+ }).pipe(Effect.tap(() => publishCommitted)));
517
+ });
518
+ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.mapError(protocolFailure("The submit request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
519
+ const identity = yield* ThreadObjectIdentity;
520
+ const receipt = yield* submit(identity.threadId, request);
446
521
  return SubmitSucceeded.make({ receipt });
447
522
  })), respond, Effect.flatMap(encodeResponse));
448
523
  const submissionStatusEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
524
+ yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
525
+ operation: "awaitSettlement",
526
+ threadId: receipt.threadId,
527
+ submissionId: receipt.submissionId
528
+ }));
529
+ yield* requireReceiptThread(receipt.threadId);
530
+ yield* requireSubmissionThread(receipt.submissionId);
449
531
  const runtime = yield* DurableAgentRuntime;
450
532
  return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
451
533
  })), respond, Effect.flatMap(encodeResponse));
452
534
  const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
535
+ yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
536
+ operation: "awaitSettlement",
537
+ threadId: receipt.threadId,
538
+ submissionId: receipt.submissionId
539
+ }));
540
+ yield* requireReceiptThread(receipt.threadId);
541
+ yield* requireSubmissionThread(receipt.submissionId);
453
542
  const settlement = yield* (yield* DurableAgentRuntime).awaitSettlement(receipt);
454
543
  return SettlementReached.make({ settlement });
455
544
  })), respond, Effect.flatMap(encodeResponse));
@@ -458,13 +547,14 @@ const awaitProgressEndpoint = (encoded) => decodeAwaitProgressRequest(encoded).p
458
547
  const runtime = yield* DurableAgentRuntime;
459
548
  const registry = yield* ProgressWaitRegistry;
460
549
  yield* Effect.scoped(Effect.gen(function* () {
461
- const cancelled = yield* registry.subscribe(request.waiterId);
550
+ const cancelled = yield* registry.subscribe(JSON.stringify([identity.threadId, request.waiterId]));
462
551
  yield* Effect.raceFirst(runtime.awaitProgress(identity.threadId, request.afterSequence), cancelled);
463
552
  }));
464
553
  return ProgressObserved.make();
465
554
  })), respond, Effect.flatMap(encodeResponse));
466
555
  const cancelProgressEndpoint = (encoded) => decodeCancelProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress cancellation could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
467
- yield* (yield* ProgressWaitRegistry).cancel(request.waiterId);
556
+ const identity = yield* ThreadObjectIdentity;
557
+ yield* (yield* ProgressWaitRegistry).cancel(JSON.stringify([identity.threadId, request.waiterId]));
468
558
  return ProgressCancelled.make();
469
559
  })), respond, Effect.flatMap(encodeResponse));
470
560
  const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(Effect.mapError(protocolFailure("The observe request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
@@ -482,18 +572,33 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
482
572
  return ObservedPage.make({ records: [...records] });
483
573
  })), respond, Effect.flatMap(encodeResponse));
484
574
  const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
575
+ yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
576
+ operation: "abort",
577
+ submissionId: command.submissionId
578
+ }));
579
+ yield* requireSubmissionThread(command.submissionId);
485
580
  const maintenance = yield* ThreadMaintenance;
486
581
  const runtime = yield* DurableAgentRuntime;
487
582
  const intent = yield* maintenance.withMutation(runtime.abort(command));
488
583
  return AbortRecorded.make({ intent });
489
584
  })), respond, Effect.flatMap(encodeResponse));
490
585
  const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
586
+ yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
587
+ operation: "resolveApproval",
588
+ submissionId: command.submissionId
589
+ }));
590
+ yield* requireSubmissionThread(command.submissionId);
491
591
  const maintenance = yield* ThreadMaintenance;
492
592
  const runtime = yield* DurableAgentRuntime;
493
593
  const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
494
594
  return ApprovalRecorded.make({ intent });
495
595
  })), respond, Effect.flatMap(encodeResponse));
496
596
  const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
597
+ yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
598
+ operation: "resolveUnknown",
599
+ submissionId: command.submissionId
600
+ }));
601
+ yield* requireSubmissionThread(command.submissionId);
497
602
  const maintenance = yield* ThreadMaintenance;
498
603
  const runtime = yield* DurableAgentRuntime;
499
604
  const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
@@ -579,7 +684,9 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
579
684
  * alarm so the mutated lane is processed promptly. Protocol anomalies answer
580
685
  * `PortFailed(PortProtocolError)`.
581
686
  */
582
- const portCallEndpoint = (encoded) => Effect.gen(function* () {
687
+ const encodePortResponseTotal = (response) => encodePortResponse(response).pipe(Effect.catch((error) => Effect.succeed(encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`))));
688
+ const portGuardFailure = (failure) => PortFailed.make({ failure: failure._tag === "LedgerError" ? failure : PortProtocolError.make({ message: "The port request is not for the addressed Thread" }) });
689
+ const portCall = (encoded) => Effect.gen(function* () {
583
690
  const ports = yield* ThreadObjectPorts;
584
691
  const maintenance = yield* ThreadMaintenance;
585
692
  const alarm = yield* DurableAlarmService;
@@ -591,10 +698,16 @@ const portCallEndpoint = (encoded) => Effect.gen(function* () {
591
698
  message: error.message
592
699
  })));
593
700
  if (decoded._tag === "failure") return encodedPortProtocolFailure(`The port request could not be decoded: ${decoded.message}`);
701
+ if (decoded.request._tag === "LedgerLookup" && decoded.request.request._tag === "SubmissionLookupById") {
702
+ const response = yield* lookupAddressedSubmission(decoded.request.request.submissionId).pipe(Effect.map((submission) => PortSucceeded.make({ result: LedgerLookupResult.make(Option.isSome(submission) ? { submission: submission.value } : {}) })), Effect.catch((failure) => Effect.succeed(portGuardFailure(failure))));
703
+ return yield* encodePortResponseTotal(response);
704
+ }
705
+ const identityCheck = yield* requirePortThread(decoded.request).pipe(Effect.result);
706
+ if (identityCheck._tag === "Failure") return yield* encodePortResponseTotal(portGuardFailure(identityCheck.failure));
594
707
  const mutating = isMutatingPortRequest(decoded.request);
595
708
  const handled = yield* (mutating ? maintenance.withMutation(ports.handle(decoded.request)) : ports.handle(decoded.request)).pipe(Effect.exit);
596
709
  if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
597
- const response = yield* encodePortResponse(handled.value).pipe(Effect.catch((error) => Effect.succeed(encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`))));
710
+ const response = yield* encodePortResponseTotal(handled.value);
598
711
  if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error)));
599
712
  return response;
600
713
  });
@@ -602,6 +715,48 @@ const wakeEndpoint = Effect.gen(function* () {
602
715
  const identity = yield* ThreadObjectIdentity;
603
716
  yield* (yield* WakeScheduler).notify(identity.threadId);
604
717
  });
718
+ /** The per-Thread wire operations supported by native and application-owned endpoints. */
719
+ const ThreadRpcOperation = Schema.Literals([
720
+ "submitEncoded",
721
+ "submissionStatusEncoded",
722
+ "awaitSettlementEncoded",
723
+ "awaitProgressEncoded",
724
+ "cancelProgressEncoded",
725
+ "observePage",
726
+ "abortEncoded",
727
+ "resolveApprovalEncoded",
728
+ "resolveUnknownEncoded",
729
+ "portCall",
730
+ "wake"
731
+ ]);
732
+ const threadRpc = {
733
+ submitEncoded: submitEndpoint,
734
+ submissionStatusEncoded: submissionStatusEndpoint,
735
+ awaitSettlementEncoded: awaitSettlementEndpoint,
736
+ awaitProgressEncoded: awaitProgressEndpoint,
737
+ cancelProgressEncoded: cancelProgressEndpoint,
738
+ observePage: observePageEndpoint,
739
+ abortEncoded: abortEndpoint,
740
+ resolveApprovalEncoded: resolveApprovalEndpoint,
741
+ resolveUnknownEncoded: resolveUnknownEndpoint,
742
+ portCall,
743
+ wake: () => wakeEndpoint
744
+ };
745
+ /**
746
+ * Bind an addressed request to its logical Thread while sharing one physical runtime. This
747
+ * validates local placement before invoking the same native handlers. Receipts, Submission
748
+ * commands and port envelopes must match this identity; progress cancellation is Thread-scoped.
749
+ * The producer comes from the actual runtime configuration, never from caller input. These
750
+ * guards supplement the existing current model/Tool and operation authorization policies.
751
+ */
752
+ const handleRpc = Effect.fn("ThreadObject.handleRpc")(function* (threadId, operation, encoded) {
753
+ if (!(yield* ThreadObjectPlacement).ownsThread(threadId)) return yield* HostProtocolError.make({ message: "The Thread belongs to another Object" });
754
+ const { producerId } = yield* DurableRuntimeConfig;
755
+ return yield* threadRpc[operation](encoded).pipe(Effect.provideService(ThreadObjectIdentity, {
756
+ threadId,
757
+ producerId
758
+ }));
759
+ });
605
760
  const alarmEndpoint = Effect.gen(function* () {
606
761
  yield* (yield* ThreadMaintenance).pass;
607
762
  });
@@ -626,7 +781,7 @@ const effectCfPlatformLayer = (namespaceBinding, rpcTracing = false) => {
626
781
  const env = yield* WorkerEnvironment;
627
782
  const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
628
783
  return ThreadObjectNamespace.of({
629
- namespace: binding,
784
+ get: (threadId) => binding.get(binding.idFromName(threadId)),
630
785
  ...rpcTracing === true ? { rpcTracing: namespaceBinding } : {}
631
786
  });
632
787
  }));
@@ -655,21 +810,11 @@ const make = (applicationLayer, options) => {
655
810
  }));
656
811
  }));
657
812
  const rpc = {
658
- submitEncoded: (encoded) => submitEndpoint(encoded),
659
- submissionStatusEncoded: (encoded) => submissionStatusEndpoint(encoded),
660
- awaitSettlementEncoded: (encoded) => awaitSettlementEndpoint(encoded),
661
- awaitProgressEncoded: (encoded) => awaitProgressEndpoint(encoded),
662
- cancelProgressEncoded: (encoded) => cancelProgressEndpoint(encoded),
663
- observePage: (encoded) => observePageEndpoint(encoded),
664
- abortEncoded: (encoded) => abortEndpoint(encoded),
665
- resolveApprovalEncoded: (encoded) => resolveApprovalEndpoint(encoded),
666
- resolveUnknownEncoded: (encoded) => resolveUnknownEndpoint(encoded),
813
+ ...threadRpc,
667
814
  explainEncoded: (encoded) => explainEndpoint(encoded),
668
815
  verifyEncoded: (encoded) => verifyEndpoint(encoded),
669
816
  retryEncoded: (encoded) => retryEndpoint(encoded),
670
- obligationsEncoded: (encoded) => obligationsEndpoint(encoded),
671
- portCall: (encoded) => portCallEndpoint(encoded),
672
- wake: () => wakeEndpoint
817
+ obligationsEncoded: (encoded) => obligationsEndpoint(encoded)
673
818
  };
674
819
  const EffectCfThreadObject = DurableObject.make(runtime, {
675
820
  ...options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {},
@@ -686,6 +831,6 @@ const make = (applicationLayer, options) => {
686
831
  return ThreadObject;
687
832
  };
688
833
  //#endregion
689
- export { make as _, AdminVerifyRequest as a, RetryExecuted as c, decodeAdminExplainRequest as d, decodeAdminResponse as f, encodeAdminResponse as g, decodeRetryCommand as h, AdminResponse as i, ThreadObject_exports as l, decodeObligationThresholds as m, AdminFailed as n, ExplainedRecovery as o, decodeAdminVerifyRequest as p, AdminFailure as r, ObligationsScanned as s, AdminExplainRequest as t, VerifiedIntegrity as u, layer as v, layerConfig as y };
834
+ export { layer as C, layerInHost as E, ThreadObjectPorts as S, layerHostConfig as T, encodeAdminResponse as _, AdminVerifyRequest as a, portCall as b, RetryExecuted as c, VerifiedIntegrity as d, decodeAdminExplainRequest as f, decodeRetryCommand as g, decodeObligationThresholds as h, AdminResponse as i, ThreadObject_exports as l, decodeAdminVerifyRequest as m, AdminFailed as n, ExplainedRecovery as o, decodeAdminResponse as p, AdminFailure as r, ObligationsScanned as s, AdminExplainRequest as t, ThreadRpcOperation as u, handleRpc as v, layerConfig as w, submit as x, make as y };
690
835
 
691
- //# sourceMappingURL=ThreadObject-KJf_5Isr.mjs.map
836
+ //# sourceMappingURL=ThreadObject-DGkXv41C.mjs.map