@effect-agent/platform-cloudflare 0.1.0-beta.14 → 0.1.0-beta.16

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
2
- import { AbortCommand, AbortIntent, AdmissionConflict, AgentBindingResolver, AppendConflict, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, CanonicalRecordEnvelope, CanonicalSequence, ConversationNotMaterialized, ConversationRead, ConversationStore, ConversationStoreError, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeploymentId, DigestError, DurableAgentRuntime, DurableRuntimeConfig, DurableRuntimeFailpoint, DurableRuntimeFailpointError, FenceRejected, IdempotencyKey, IntegrityReport, JoinedToHost, LedgerError, ObligationReport, ObligationThresholds, OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, OwnershipLost, PersistedJson, Principal, ProducerId, Receipt, RecoveryExplanation, RecoveryReport, RetryCommand, RetryRefused, RunJournalError, Settlement, SettlementConflict, SubmissionLedger, SubmissionLookupByKey, ToolReconciler, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, WakeScheduler } from "@effect-agent/session";
1
+ import { Cause, Clock, Context, Crypto, Deferred, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Semaphore, Stream } from "effect";
2
+ import { AbortCommand, AbortIntent, AdmissionConflict, AgentBindingResolver, AppendConflict, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, CanonicalRecordEnvelope, CanonicalSequence, ConversationNotMaterialized, ConversationRead, ConversationStore, ConversationStoreError, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeploymentId, DigestError, DurableAgentRuntime, DurableRuntimeConfig, DurableRuntimeFailpoint, DurableRuntimeFailpointError, FenceRejected, IdempotencyKey, IntegrityReport, JoinedToHost, LedgerError, ObligationReport, ObligationThresholds, OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, OwnershipLost, PersistedJson, Principal, ProducerId, Receipt, RecoveryExplanation, RecoveryReport, RetryCommand, RetryRefused, RunJournalError, Settlement, SettlementConflict, SubmissionLedger, SubmissionLookupByKey, ToolReconciler, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, WakeScheduler, makeWakeSubscriptionHub, operationAuthorizerLayer } from "@effect-agent/session";
3
3
  import { ConversationPortTransport, DEFAULT_MAX_STORED_VALUE_BYTES, conversationStoreLayer, handleEncodedPortRequest, portTransportFailure, routedConversationStoreLayer, routedSubmissionLedgerLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer } from "@effect-agent/storage-cloudflare";
4
4
  import { AgentId, AgentInputError, ConversationId, SubmissionId } from "@effect-agent/core";
5
5
  import { BrowserCrypto } from "@effect/platform-browser";
@@ -180,11 +180,10 @@ const CLOUDFLARE_RUNTIME_DEFAULTS = {
180
180
  * and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
181
181
  * the slot always holds the EARLIEST deadline any caller asked for.
182
182
  *
183
- * The alarm invariant (plan §1.4): committed nonterminal work implies a committed alarm.
184
- * It is established by pre-arming every mutating entry point and every pass arms the alarm
185
- * BEFORE its durable mutations — so an eviction at any failpoint leaves a persisted alarm
186
- * that workerd re-delivers to a fresh incarnation WITHOUT any incoming request. Spurious
187
- * alarms are harmless by design: a pass over an all-settled lane simply deletes the slot.
183
+ * The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
184
+ * maintenance generation and a committed alarm. Stable externally-driven waits may be
185
+ * nonterminal without retaining an alarm; their resolving mutation advances the generation and
186
+ * restores the alarm atomically.
188
187
  */
189
188
  /** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
190
189
  var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
@@ -207,7 +206,6 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
207
206
  * on top of the already-committed pre-armed alarm.
208
207
  */
209
208
  const runningPasses = yield* Ref.make(0);
210
- const wakeDeferred = yield* Ref.make(false);
211
209
  const scheduled = Effect.tryPromise({
212
210
  try: () => ctx.storage.getAlarm(),
213
211
  catch: alarmFailure("get alarm")
@@ -218,14 +216,8 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
218
216
  });
219
217
  const ensureScheduledBy = (epochMillis) => scheduled.pipe(Effect.flatMap((existing) => Option.isSome(existing) && existing.value <= epochMillis ? Effect.void : scheduleAt(epochMillis)));
220
218
  const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
221
- const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Ref.set(wakeDeferred, true) : armNow));
222
- /**
223
- * Flush after the LAST concurrent pass: the re-arm lands at the very end of the alarm
224
- * handler, where a superseding cancellation only re-delivers to the already-idempotent
225
- * pass. Runs inside `Effect.ensuring`, so flush failures are logged, never raised.
226
- */
227
- const flushDeferredWake = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : Ref.getAndSet(wakeDeferred, false).pipe(Effect.flatMap((wanted) => wanted ? armNow : Effect.void))), Effect.catch((error) => Effect.logWarning("DurableAlarmService: deferred wake flush failed", error)));
228
- const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1).pipe(Effect.andThen(flushDeferredWake))));
219
+ const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
220
+ const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
229
221
  const cancel = Effect.tryPromise({
230
222
  try: () => ctx.storage.deleteAlarm(),
231
223
  catch: alarmFailure("delete alarm")
@@ -242,35 +234,78 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
242
234
  };
243
235
  /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
244
236
  var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
237
+ /** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */
238
+ phase: Schema.Literals(["caught-up", "actionable"]),
245
239
  /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
246
240
  recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
247
241
  /** Settlements the drain pass finalized. */
248
242
  settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
249
243
  /** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
250
244
  nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
251
- /** `rearmed` while nonterminal work remains, `cleared` once everything settled. */
245
+ /** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
252
246
  alarm: Schema.Literals(["rearmed", "cleared"])
253
247
  }) {};
248
+ /** Test-only fault authority; production uses the inert layer. */
249
+ var ConversationMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ConversationMaintenanceFailpoint") {
250
+ static layer = Layer.succeed(this)({ hit: () => Effect.void });
251
+ };
252
+ const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
253
+ /** Versioned, platform-private maintenance state stored through Durable Object KV. */
254
+ var ConversationMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ConversationMaintenanceState")({
255
+ schemaVersion: Schema.Literal(1),
256
+ dirty: MaintenanceGeneration,
257
+ processed: MaintenanceGeneration,
258
+ nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
259
+ }) {};
260
+ const MAINTENANCE_STATE_KEY = "effect-agent:conversation-maintenance:v1";
261
+ const decodeMaintenanceState = Schema.decodeUnknownSync(ConversationMaintenanceState);
262
+ const encodeMaintenanceState = Schema.encodeSync(ConversationMaintenanceState);
263
+ const initialMaintenanceState = () => ConversationMaintenanceState.make({
264
+ schemaVersion: 1,
265
+ dirty: 1n,
266
+ processed: 0n,
267
+ nonterminal: 0
268
+ });
269
+ const readMaintenanceState = async (transaction) => {
270
+ const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
271
+ return encoded === void 0 ? {
272
+ state: initialMaintenanceState(),
273
+ initialized: false
274
+ } : {
275
+ state: decodeMaintenanceState(encoded),
276
+ initialized: true
277
+ };
278
+ };
279
+ const ensureTransactionAlarmBy = async (transaction, deadline) => {
280
+ const scheduled = await transaction.getAlarm();
281
+ if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
282
+ };
283
+ const stableExternalWait = (snapshot, reports) => {
284
+ switch (snapshot.state) {
285
+ case "suspended":
286
+ case "unknown":
287
+ case "joined": return true;
288
+ case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
289
+ case "input-applied":
290
+ case "joining":
291
+ case "ready":
292
+ case "running":
293
+ case "settled":
294
+ case "terminalizing": return false;
295
+ }
296
+ };
254
297
  /**
255
- * The idempotent maintenance pass and the alarm-invariant helpers (plan §1.4, D-P6-1/2).
256
- *
257
- * `pass` = pre-arm → `runRecovery` → `processConversationResolved` → re-arm-or-clear:
298
+ * Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
258
299
  *
259
- * 1. **Pre-arm**: the alarm is re-armed at `now + wakeScanInterval` BEFORE any work, so an
260
- * eviction (or thrown failure → workerd alarm retry) at any point of the pass leaves a
261
- * committed alarm and the fresh incarnation converges without an incoming request.
262
- * 2. **Reconcile before new work** (exit gate): every pass classifies and repairs every
263
- * nonterminal Submission before the drain claims anything; the pure classifier and the
264
- * repair executors are idempotent, so at-least-once alarm delivery re-runs them safely.
265
- * 3. **Drain**: one bounded `processConversationResolved` pass over this Object's lane — the
266
- * D-P6-1 shape; no infinite `runResolvedWorker` loop ever pins the Object.
267
- * 4. **Re-arm policy**: nonterminal work re-arms at `now + min(backoff-with-jitter,
268
- * wakeScanInterval)` — the scan interval bounds every wait (lease expiry of a dead
269
- * incarnation included, since claims retry each pass) and the backoff (reset on progress,
270
- * grown otherwise) keeps stuck lanes from busy-spinning; all settled clears the slot.
300
+ * `pass` = generation snapshot/pre-arm recovery bounded drain generation acknowledgement:
271
301
  *
272
- * Every step is a durability-protocol step that already tolerates re-execution, which is
273
- * what makes double-fired alarms harmless (the alarm.test.ts gate).
302
+ * 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
303
+ * alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
304
+ * 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.
305
+ * 3. The final transaction acknowledges only the generation observed at pass start. A racing
306
+ * mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
307
+ * 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
308
+ * recovery states leave their generation dirty and retain bounded backoff rearming.
274
309
  */
275
310
  var ConversationMaintenance = class ConversationMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ConversationMaintenance") {
276
311
  static layer = Layer.effect(ConversationMaintenance)(Effect.gen(function* () {
@@ -280,13 +315,76 @@ var ConversationMaintenance = class ConversationMaintenance extends Context.Serv
280
315
  const alarm = yield* DurableAlarmService;
281
316
  const config = yield* CloudflareDurableRuntimeConfig;
282
317
  const identity = yield* ConversationObjectIdentity;
318
+ const { ctx } = yield* DurableObjectContext;
319
+ const failpoint = yield* ConversationMaintenanceFailpoint;
283
320
  /**
284
321
  * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
285
322
  * restarts at zero and merely re-arms sooner than a long-lived one would have.
286
323
  */
287
324
  const stalls = yield* Ref.make(0);
288
- const preArm = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => alarm.ensureScheduledBy(now + config.wakeScanInterval)));
289
- const countNonterminal = Stream.runCollect(ledger.scanNonterminal).pipe(Effect.map((snapshots) => snapshots.length));
325
+ /**
326
+ * Incarnation-local mutation count guarded with the generation transactions below. It is
327
+ * deliberately not durable: after eviction every begun mutation has stopped, while its
328
+ * pre-armed dirty generation remains durable for recovery. The short gate never spans the
329
+ * caller's mutation or cross-Object I/O.
330
+ */
331
+ const activeMutations = yield* Ref.make(0);
332
+ const generationGate = yield* Semaphore.make(1);
333
+ const maintenancePassGate = yield* Semaphore.make(1);
334
+ const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
335
+ const runTransaction = (operation, transaction) => Effect.tryPromise({
336
+ try: transaction,
337
+ catch: alarmFailure(operation)
338
+ });
339
+ const beginMutation = Effect.fn("ConversationMaintenance.beginMutation")(function* () {
340
+ yield* failpoint.hit("maintenance:dirty:before");
341
+ const now = yield* Clock.currentTimeMillis;
342
+ yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
343
+ const { state } = await readMaintenanceState(transaction);
344
+ const next = ConversationMaintenanceState.make({
345
+ ...state,
346
+ dirty: state.dirty + 1n
347
+ });
348
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
349
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
350
+ }));
351
+ yield* failpoint.hit("maintenance:dirty:after");
352
+ yield* Ref.update(activeMutations, (active) => active + 1);
353
+ });
354
+ const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
355
+ const withMutation = (body) => Effect.acquireUseRelease(generationGate.withPermit(beginMutation()), () => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(body), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), () => endMutation);
356
+ const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
357
+ yield* failpoint.hit("maintenance:ensure:before");
358
+ const now = yield* Clock.currentTimeMillis;
359
+ yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
360
+ const { state, initialized } = await readMaintenanceState(transaction);
361
+ if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
362
+ if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
363
+ }));
364
+ yield* failpoint.hit("maintenance:ensure:after");
365
+ });
366
+ const beginPass = Effect.fn("ConversationMaintenance.beginPass")(function* () {
367
+ yield* failpoint.hit("maintenance:begin:before");
368
+ const now = yield* Clock.currentTimeMillis;
369
+ const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
370
+ const { state, initialized } = await readMaintenanceState(transaction);
371
+ if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
372
+ if (state.processed >= state.dirty) {
373
+ await transaction.deleteAlarm();
374
+ return {
375
+ _tag: "CaughtUp",
376
+ nonterminal: state.nonterminal
377
+ };
378
+ }
379
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
380
+ return {
381
+ _tag: "Actionable",
382
+ generation: state.dirty
383
+ };
384
+ }));
385
+ yield* failpoint.hit("maintenance:begin:after");
386
+ return result;
387
+ });
290
388
  const rearmDelay = Effect.fn("ConversationMaintenance.rearmDelay")(function* (progressed) {
291
389
  const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
292
390
  if (progressed) return config.alarmBackoffBase;
@@ -297,49 +395,73 @@ var ConversationMaintenance = class ConversationMaintenance extends Context.Serv
297
395
  return Math.min(jittered, config.wakeScanInterval);
298
396
  });
299
397
  const pass = Effect.fn("ConversationMaintenance.pass")(function* () {
300
- yield* preArm;
398
+ const annotate = (report) => Effect.annotateCurrentSpan({
399
+ phase: report.phase,
400
+ recovered: report.recovered,
401
+ settled: report.settled,
402
+ nonterminal: report.nonterminal,
403
+ alarm: report.alarm
404
+ }).pipe(Effect.as(report));
405
+ const started = yield* generationGate.withPermit(Effect.gen(function* () {
406
+ const activeAtStart = yield* Ref.get(activeMutations);
407
+ return {
408
+ ...yield* beginPass(),
409
+ activeAtStart
410
+ };
411
+ }));
412
+ if (started._tag === "CaughtUp") return yield* annotate(MaintenancePassReport.make({
413
+ phase: "caught-up",
414
+ recovered: 0,
415
+ settled: 0,
416
+ nonterminal: started.nonterminal,
417
+ alarm: "cleared"
418
+ }));
301
419
  const recovered = yield* runtime.runRecovery;
302
420
  const settlements = yield* runtime.processConversationResolved(identity.conversationId).pipe(Effect.provideService(AgentBindingResolver, resolver));
303
- const nonterminal = yield* countNonterminal;
304
- if (nonterminal === 0) {
305
- yield* alarm.cancel;
306
- yield* Ref.set(stalls, 0);
307
- const appeared = yield* countNonterminal;
308
- if (appeared > 0) {
309
- yield* preArm;
310
- return MaintenancePassReport.make({
311
- recovered: recovered.length,
312
- settled: settlements.length,
313
- nonterminal: appeared,
314
- alarm: "rearmed"
315
- });
316
- }
317
- return MaintenancePassReport.make({
318
- recovered: recovered.length,
319
- settled: settlements.length,
320
- nonterminal,
321
- alarm: "cleared"
322
- });
323
- }
421
+ const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
422
+ const reports = new Map(recovered.map((report) => [report.submissionId, report]));
423
+ const autonomous = remaining.some((snapshot) => !stableExternalWait(snapshot, reports));
324
424
  const progressed = settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
325
- const delay = yield* rearmDelay(progressed);
425
+ const delay = autonomous ? yield* rearmDelay(progressed) : 0;
326
426
  const now = yield* Clock.currentTimeMillis;
327
- yield* alarm.ensureScheduledBy(now + delay);
328
- return MaintenancePassReport.make({
427
+ yield* failpoint.hit("maintenance:finish:before");
428
+ const alarmDisposition = yield* generationGate.withPermit(Effect.gen(function* () {
429
+ const active = yield* Ref.get(activeMutations);
430
+ return yield* runTransaction("finish maintenance pass", () => ctx.storage.transaction(async (transaction) => {
431
+ const { state } = await readMaintenanceState(transaction);
432
+ const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
433
+ const next = ConversationMaintenanceState.make({
434
+ ...state,
435
+ processed,
436
+ nonterminal: remaining.length
437
+ });
438
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
439
+ if (autonomous) {
440
+ await transaction.setAlarm(now + delay);
441
+ return "rearmed";
442
+ }
443
+ if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
444
+ await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
445
+ return "rearmed";
446
+ }
447
+ await transaction.deleteAlarm();
448
+ return "cleared";
449
+ }));
450
+ }));
451
+ yield* failpoint.hit("maintenance:finish:after");
452
+ if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
453
+ return yield* annotate(MaintenancePassReport.make({
454
+ phase: "actionable",
329
455
  recovered: recovered.length,
330
456
  settled: settlements.length,
331
- nonterminal,
332
- alarm: "rearmed"
333
- });
334
- });
335
- const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
336
- if ((yield* countNonterminal) === 0) return;
337
- yield* preArm;
457
+ nonterminal: remaining.length,
458
+ alarm: alarmDisposition
459
+ }));
338
460
  });
339
461
  return ConversationMaintenance.of({
340
- pass: alarm.withWakesDeferred(pass()),
462
+ pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
341
463
  ensureAlarm: ensureAlarm(),
342
- preArm
464
+ withMutation
343
465
  });
344
466
  }));
345
467
  };
@@ -375,8 +497,9 @@ const cloudflareWakeSchedulerLayer = Layer.effect(WakeScheduler)(Effect.gen(func
375
497
  const identity = yield* ConversationObjectIdentity;
376
498
  const { namespace } = yield* ConversationObjectNamespace;
377
499
  const hints = yield* PubSub.sliding(WAKE_BUFFER_CAPACITY);
500
+ const progress = yield* makeWakeSubscriptionHub;
378
501
  yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
379
- const notifyLocal = (conversationId) => PubSub.publish(hints, conversationId).pipe(Effect.andThen(alarm.scheduleNow), Effect.catch((error) => Effect.logWarning("CloudflareWakeScheduler: local alarm wake failed", error)), Effect.asVoid);
502
+ const notifyLocal = (conversationId) => progress.notify(conversationId).pipe(Effect.andThen(PubSub.publish(hints, conversationId)), Effect.andThen(alarm.scheduleNow), Effect.catch((error) => Effect.logWarning("CloudflareWakeScheduler: local alarm wake failed", error)), Effect.asVoid);
380
503
  const notifyRemote = (conversationId) => Effect.tryPromise({
381
504
  try: () => namespace.get(namespace.idFromName(conversationId)).wake(),
382
505
  catch: (cause) => RemoteWakeDropped.make({
@@ -387,10 +510,76 @@ const cloudflareWakeSchedulerLayer = Layer.effect(WakeScheduler)(Effect.gen(func
387
510
  }).pipe(Effect.catch((error) => Effect.logWarning(`CloudflareWakeScheduler: remote wake of ${conversationId} dropped`, error)), Effect.asVoid);
388
511
  return WakeScheduler.of({
389
512
  notify: (conversationId) => conversationId === identity.conversationId ? notifyLocal(conversationId) : notifyRemote(conversationId),
513
+ subscribe: progress.subscribe,
390
514
  wakes: Stream.fromPubSub(hints)
391
515
  });
392
516
  }));
393
517
  //#endregion
518
+ //#region src/progress-wait.ts
519
+ /** Cancellation tombstones are bounded hints, never durable authority. */
520
+ const MAX_CANCELLATION_TOMBSTONES = 1024;
521
+ /**
522
+ * Per-incarnation cancellation registry for long-lived progress RPCs. The public runtime owns
523
+ * the actual wake registration; this host-only registry lets an interrupted Worker Effect ask
524
+ * the Object to interrupt its scoped wait before the Worker execution context itself ends.
525
+ */
526
+ var ProgressWaitRegistry = class ProgressWaitRegistry extends Context.Service()("@effect-agent/platform-cloudflare/ProgressWaitRegistry") {
527
+ static layer = Layer.effect(ProgressWaitRegistry, Effect.gen(function* () {
528
+ const registrations = yield* Ref.make(/* @__PURE__ */ new Map());
529
+ const remove = (waiterId, deferred) => Ref.update(registrations, (current) => {
530
+ const existing = current.get(waiterId);
531
+ if (existing === void 0 || existing === "cancelled" || !existing.has(deferred)) return current;
532
+ const next = new Map(current);
533
+ const active = new Set(existing);
534
+ active.delete(deferred);
535
+ if (active.size === 0) next.delete(waiterId);
536
+ else next.set(waiterId, active);
537
+ return next;
538
+ });
539
+ const subscribe = Effect.fn("ProgressWaitRegistry.subscribe")((waiterId) => Effect.gen(function* () {
540
+ const deferred = yield* Deferred.make();
541
+ yield* Effect.addFinalizer(() => remove(waiterId, deferred));
542
+ return {
543
+ cancelled: yield* Ref.modify(registrations, (current) => {
544
+ const existing = current.get(waiterId);
545
+ const next = new Map(current);
546
+ if (existing === "cancelled") return [true, current];
547
+ const active = new Set(existing ?? []);
548
+ active.add(deferred);
549
+ next.set(waiterId, active);
550
+ return [false, next];
551
+ }),
552
+ deferred
553
+ };
554
+ }).pipe(Effect.map(({ cancelled, deferred }) => cancelled ? Effect.void : Deferred.await(deferred))));
555
+ const cancel = Effect.fn("ProgressWaitRegistry.cancel")(function* (waiterId) {
556
+ const waiters = yield* Ref.modify(registrations, (current) => {
557
+ const existing = current.get(waiterId);
558
+ const next = new Map(current);
559
+ if (existing === void 0) {
560
+ next.set(waiterId, "cancelled");
561
+ let tombstones = 0;
562
+ for (const registration of next.values()) if (registration === "cancelled") tombstones += 1;
563
+ if (tombstones > MAX_CANCELLATION_TOMBSTONES) for (const [id, registration] of next) {
564
+ if (registration !== "cancelled") continue;
565
+ next.delete(id);
566
+ break;
567
+ }
568
+ return [[], next];
569
+ }
570
+ if (existing === "cancelled") return [[], current];
571
+ next.delete(waiterId);
572
+ return [[...existing], next];
573
+ });
574
+ yield* Effect.forEach(waiters, (waiter) => Deferred.succeed(waiter, void 0), { discard: true });
575
+ });
576
+ return ProgressWaitRegistry.of({
577
+ subscribe,
578
+ cancel
579
+ });
580
+ }));
581
+ };
582
+ //#endregion
394
583
  //#region src/transport.ts
395
584
  /**
396
585
  * `ConversationPortTransport` over native Durable Object JS RPC (decision D-P6-3): one
@@ -519,15 +708,17 @@ var CloudflareDurableRuntime = class {
519
708
  abortPollInterval: Duration.millis(config.abortPollInterval)
520
709
  }));
521
710
  const runtimeFailpointLayer = options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
711
+ const maintenanceFailpointLayer = options.maintenanceFailpoint === void 0 ? ConversationMaintenanceFailpoint.layer : Layer.succeed(ConversationMaintenanceFailpoint)({ hit: options.maintenanceFailpoint(ctx) });
522
712
  const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
713
+ const authorizerLayer = options.operationAuthorizer === void 0 ? Layer.empty : operationAuthorizerLayer(options.operationAuthorizer);
523
714
  const bindingResolverLayer = Layer.effect(AgentBindingResolver)(Effect.map(resolveBindings(options.bindings, {
524
715
  ctx,
525
716
  env,
526
717
  conversationId,
527
718
  producerId
528
719
  }), (bindings) => AgentBindingResolver.fromBindings(bindings)));
529
- const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer);
530
- const runtimeStack = DurableAgentRuntime.layer.pipe(Layer.provideMerge(routedPorts), Layer.provideMerge(cloudflareWakeSchedulerLayer), Layer.provideMerge(runtimeConfigLayer), Layer.provideMerge(bindingResolverLayer), Layer.provide(Layer.mergeAll(runtimeFailpointLayer, reconcilerLayer, BrowserCrypto.layer)), Layer.provideMerge(base));
720
+ const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer, maintenanceFailpointLayer, ProgressWaitRegistry.layer);
721
+ const runtimeStack = DurableAgentRuntime.layer.pipe(Layer.provideMerge(routedPorts), Layer.provideMerge(cloudflareWakeSchedulerLayer), Layer.provideMerge(runtimeConfigLayer), Layer.provideMerge(bindingResolverLayer), Layer.provide(Layer.mergeAll(runtimeFailpointLayer, reconcilerLayer, authorizerLayer, BrowserCrypto.layer)), Layer.provideMerge(base));
531
722
  return Layer.mergeAll(runtimeStack, ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)), portsEndpointLayer);
532
723
  }));
533
724
  }
@@ -556,7 +747,11 @@ var HostProtocolError = class extends Schema.TaggedError()("HostProtocolError",
556
747
  var ConversationClientError = class extends Schema.TaggedError()("ConversationClientError", {
557
748
  conversationId: Schema.String,
558
749
  message: Schema.String,
559
- cause: Schema.optionalKey(Schema.Defect())
750
+ cause: Schema.optionalKey(Schema.Defect()),
751
+ /** Cloudflare's own classification for a failure safe to retry with a fresh stub. */
752
+ retryable: Schema.optionalKey(Schema.Boolean),
753
+ /** Cloudflare overloads are surfaced immediately instead of adding retry pressure. */
754
+ overloaded: Schema.optionalKey(Schema.Boolean)
560
755
  }) {};
561
756
  /**
562
757
  * One durable submission, input ALREADY encoded by the caller through the Agent Binding's
@@ -576,6 +771,13 @@ var ObservePageRequest = class extends Schema.Class("@effect-agent/platform-clou
576
771
  afterSequence: Schema.optionalKey(CanonicalSequence),
577
772
  limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
578
773
  }) {};
774
+ /** One event-driven wait for canonical progress strictly after this sequence. */
775
+ var AwaitProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/AwaitProgressRequest")({
776
+ afterSequence: CanonicalSequence,
777
+ waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256))
778
+ }) {};
779
+ /** Best-effort cancellation of one in-flight progress RPC. */
780
+ var CancelProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/CancelProgressRequest")({ waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)) }) {};
579
781
  /**
580
782
  * Every typed failure a host entry point can produce, plus the protocol's own errors. Same
581
783
  * closed-union discipline as the WP2 `PortFailure`: members re-decode to the SAME tagged
@@ -598,11 +800,15 @@ const HostFailure = Schema.Union([
598
800
  DurableRuntimeFailpointError,
599
801
  AdmissionLimitExceeded,
600
802
  DurableAlarmError,
803
+ OperationDenied,
601
804
  HostProtocolError
602
805
  ]);
603
806
  var SubmitSucceeded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SubmitSucceeded")("SubmitSucceeded", { receipt: Receipt }) {};
604
807
  var SettlementReached = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SettlementReached")("SettlementReached", { settlement: Settlement }) {};
605
808
  var ObservedPage = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ObservedPage")("ObservedPage", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
809
+ /** A record was already committed or an incarnation-local hint says the caller should re-read. */
810
+ var ProgressObserved = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressObserved")("ProgressObserved", {}) {};
811
+ var ProgressCancelled = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressCancelled")("ProgressCancelled", {}) {};
606
812
  var AbortRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/AbortRecorded")("AbortRecorded", { intent: AbortIntent }) {};
607
813
  var ApprovalRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ApprovalRecorded")("ApprovalRecorded", { intent: ApprovalDecisionIntent }) {};
608
814
  var UnknownResolutionRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/UnknownResolutionRecorded")("UnknownResolutionRecorded", { intent: UnknownResolutionIntent }) {};
@@ -613,6 +819,8 @@ const HostResponse = Schema.Union([
613
819
  SubmitSucceeded,
614
820
  SettlementReached,
615
821
  ObservedPage,
822
+ ProgressObserved,
823
+ ProgressCancelled,
616
824
  AbortRecorded,
617
825
  ApprovalRecorded,
618
826
  UnknownResolutionRecorded,
@@ -624,6 +832,10 @@ const decodeReceipt = Schema.decodeUnknownEffect(Receipt);
624
832
  const encodeReceipt = Schema.encodeEffect(Receipt);
625
833
  const decodeObservePageRequest = Schema.decodeUnknownEffect(ObservePageRequest);
626
834
  const encodeObservePageRequest = Schema.encodeEffect(ObservePageRequest);
835
+ const decodeAwaitProgressRequest = Schema.decodeUnknownEffect(AwaitProgressRequest);
836
+ const encodeAwaitProgressRequest = Schema.encodeEffect(AwaitProgressRequest);
837
+ const decodeCancelProgressRequest = Schema.decodeUnknownEffect(CancelProgressRequest);
838
+ const encodeCancelProgressRequest = Schema.encodeEffect(CancelProgressRequest);
627
839
  const decodeAbortCommand = Schema.decodeUnknownEffect(AbortCommand);
628
840
  const encodeAbortCommand = Schema.encodeEffect(AbortCommand);
629
841
  const decodeApprovalDecisionCommand = Schema.decodeUnknownEffect(ApprovalDecisionCommand);
@@ -654,8 +866,10 @@ const AWAIT_FAILURE_TAGS = /* @__PURE__ */ new Set([
654
866
  const OBSERVE_FAILURE_TAGS = /* @__PURE__ */ new Set([
655
867
  "ConversationStoreError",
656
868
  "ConversationNotMaterialized",
869
+ "OperationDenied",
657
870
  "HostProtocolError"
658
871
  ]);
872
+ const PROGRESS_FAILURE_TAGS = OBSERVE_FAILURE_TAGS;
659
873
  const ABORT_FAILURE_TAGS = /* @__PURE__ */ new Set([
660
874
  "LedgerError",
661
875
  "SettlementConflict",
@@ -668,6 +882,7 @@ const APPROVAL_FAILURE_TAGS = /* @__PURE__ */ new Set([
668
882
  "LedgerError",
669
883
  "SettlementConflict",
670
884
  "ApprovalConflict",
885
+ "OperationDenied",
671
886
  "DurableAlarmError",
672
887
  "HostProtocolError"
673
888
  ]);
@@ -677,6 +892,7 @@ const UNKNOWN_FAILURE_TAGS = /* @__PURE__ */ new Set([
677
892
  "UnknownResolutionConflict",
678
893
  "JoinedToHost",
679
894
  "DurableRuntimeFailpointError",
895
+ "OperationDenied",
680
896
  "DurableAlarmError",
681
897
  "HostProtocolError"
682
898
  ]);
@@ -695,12 +911,27 @@ const narrowFailure = (tags) => (failure) => tags.has(failure._tag);
695
911
  var CloudflareConversationClient = class CloudflareConversationClient extends Context.Service()("@effect-agent/platform-cloudflare/CloudflareConversationClient") {
696
912
  static layer = Layer.effect(CloudflareConversationClient)(Effect.gen(function* () {
697
913
  const { namespace } = yield* ConversationObjectNamespace;
914
+ const crypto = yield* Crypto.Crypto;
915
+ const platformSignals = (cause) => {
916
+ let retryable;
917
+ let overloaded;
918
+ if (typeof cause === "object" && cause !== null) {
919
+ if ("retryable" in cause && typeof cause.retryable === "boolean") retryable = cause.retryable;
920
+ if ("overloaded" in cause && typeof cause.overloaded === "boolean") overloaded = cause.overloaded;
921
+ if (retryable === void 0 && "durableObjectReset" in cause && cause.durableObjectReset === true) retryable = true;
922
+ }
923
+ return {
924
+ ...retryable === void 0 ? {} : { retryable },
925
+ ...overloaded === void 0 ? {} : { overloaded }
926
+ };
927
+ };
698
928
  const call = (conversationId, operation, invoke) => Effect.tryPromise({
699
929
  try: () => invoke(namespace.get(namespace.idFromName(conversationId))),
700
930
  catch: (cause) => ConversationClientError.make({
701
931
  conversationId,
702
932
  message: boundHostDiagnostic(`${operation} did not reach the Conversation Object: ${cause instanceof Error ? cause.message : String(cause)}`),
703
- cause
933
+ cause,
934
+ ...platformSignals(cause)
704
935
  })
705
936
  }).pipe(Effect.flatMap((raw) => decodeHostResponse(raw).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`The ${operation} answer could not be decoded: ${error.message}`) })))), Effect.withSpan("CloudflareConversationClient.call", { attributes: {
706
937
  conversationId,
@@ -726,6 +957,7 @@ var CloudflareConversationClient = class CloudflareConversationClient extends Co
726
957
  const response = yield* call(conversationId, "observePage", (stub) => stub.observePage(encoded));
727
958
  return (yield* expect(conversationId, "observePage", "ObservedPage", OBSERVE_FAILURE_TAGS)(response)).records;
728
959
  });
960
+ const cancelProgress = (conversationId, waiterId) => encodeCancelProgressRequest(CancelProgressRequest.make({ waiterId })).pipe(Effect.mapError(() => void 0), Effect.flatMap((encoded) => call(conversationId, "cancelProgress", (stub) => stub.cancelProgressEncoded(encoded))), Effect.asVoid, Effect.ignore);
729
961
  return CloudflareConversationClient.of({
730
962
  submit: (agent, input, options) => Effect.gen(function* () {
731
963
  const encodedInput = yield* Schema.encodeEffect(agent.definition.input)(input).pipe(Effect.mapError((cause) => AgentInputError.make({ message: `Unable to encode Agent input: ${cause.message}` })));
@@ -746,6 +978,16 @@ var CloudflareConversationClient = class CloudflareConversationClient extends Co
746
978
  const response = yield* call(receipt.conversationId, "awaitSettlement", (stub) => stub.awaitSettlementEncoded(encoded));
747
979
  return (yield* expect(receipt.conversationId, "awaitSettlement", "SettlementReached", AWAIT_FAILURE_TAGS)(response)).settlement;
748
980
  }),
981
+ awaitProgress: (conversationId, afterSequence) => Effect.gen(function* () {
982
+ const waiterId = yield* crypto.randomUUIDv4.pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress cancellation identity generation failed: ${error.message}`) })));
983
+ const request = AwaitProgressRequest.make({
984
+ afterSequence,
985
+ waiterId
986
+ });
987
+ const encoded = yield* encodeAwaitProgressRequest(request).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress request encode failed: ${error.message}`) })));
988
+ const attempt = (retry) => call(conversationId, "awaitProgress", (stub) => stub.awaitProgressEncoded(encoded)).pipe(Effect.flatMap(expect(conversationId, "awaitProgress", "ProgressObserved", PROGRESS_FAILURE_TAGS)), Effect.asVoid, Effect.catchTag("ConversationClientError", (error) => error.retryable === true && error.overloaded !== true && retry < 5 ? Effect.sleep(Duration.millis(10 * 2 ** retry)).pipe(Effect.andThen(attempt(retry + 1))) : Effect.fail(error)));
989
+ yield* attempt(0).pipe(Effect.onInterrupt(() => cancelProgress(conversationId, waiterId)));
990
+ }),
749
991
  readPage,
750
992
  readAll: (conversationId) => Effect.gen(function* () {
751
993
  const all = [];
@@ -862,26 +1104,39 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
862
1104
  const maintenance = yield* ConversationMaintenance;
863
1105
  const runtime = yield* DurableAgentRuntime;
864
1106
  yield* gateAdmissionLimits(request);
865
- yield* maintenance.preArm;
866
- const receipt = yield* runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
1107
+ const receipt = yield* maintenance.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
867
1108
  conversationId: identity.conversationId,
868
1109
  principal: request.principal,
869
1110
  idempotencyKey: request.idempotencyKey,
870
1111
  definitions: request.definitions
871
- });
1112
+ }));
872
1113
  return SubmitSucceeded.make({ receipt });
873
1114
  })), respond, Effect.flatMap(encodeResponse));
874
1115
  const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
875
1116
  const settlement = yield* (yield* DurableAgentRuntime).awaitSettlement(receipt);
876
1117
  return SettlementReached.make({ settlement });
877
1118
  })), respond, Effect.flatMap(encodeResponse));
1119
+ const awaitProgressEndpoint = (encoded) => decodeAwaitProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
1120
+ const identity = yield* ConversationObjectIdentity;
1121
+ const runtime = yield* DurableAgentRuntime;
1122
+ const registry = yield* ProgressWaitRegistry;
1123
+ yield* Effect.scoped(Effect.gen(function* () {
1124
+ const cancelled = yield* registry.subscribe(request.waiterId);
1125
+ yield* Effect.raceFirst(runtime.awaitProgress(identity.conversationId, request.afterSequence), cancelled);
1126
+ }));
1127
+ return ProgressObserved.make();
1128
+ })), respond, Effect.flatMap(encodeResponse));
1129
+ const cancelProgressEndpoint = (encoded) => decodeCancelProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress cancellation could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
1130
+ yield* (yield* ProgressWaitRegistry).cancel(request.waiterId);
1131
+ return ProgressCancelled.make();
1132
+ })), respond, Effect.flatMap(encodeResponse));
878
1133
  const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(Effect.mapError(protocolFailure("The observe request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
879
1134
  const identity = yield* ConversationObjectIdentity;
880
1135
  const store = yield* ConversationStore;
881
1136
  yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
882
1137
  operation: "observe",
883
1138
  conversationId: identity.conversationId
884
- })).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
1139
+ }));
885
1140
  const records = yield* Stream.runCollect(store.read(ConversationRead.make({
886
1141
  conversationId: identity.conversationId,
887
1142
  ...request.afterSequence === void 0 ? {} : { afterSequence: request.afterSequence },
@@ -892,30 +1147,19 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
892
1147
  const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
893
1148
  const maintenance = yield* ConversationMaintenance;
894
1149
  const runtime = yield* DurableAgentRuntime;
895
- yield* maintenance.preArm;
896
- const intent = yield* runtime.abort(command);
1150
+ const intent = yield* maintenance.withMutation(runtime.abort(command));
897
1151
  return AbortRecorded.make({ intent });
898
1152
  })), respond, Effect.flatMap(encodeResponse));
899
- /**
900
- * The pre-P7 host protocol's failure union does not carry `OperationDenied` (the Worker client
901
- * predates the authorizer). This assembly always runs the default possession authorizer — no
902
- * `CloudflareDurableRuntimeOptions` authorizer lever exists yet — so a denial here is
903
- * unreachable today; if one ever surfaces it degrades to the protocol failure instead of an
904
- * out-of-contract throw. The four P7 admin entry points below carry `OperationDenied` typed.
905
- */
906
- const deniedToProtocolFailure = (denied) => Effect.fail(HostProtocolError.make({ message: boundHostDiagnostic(`The ${denied.operation} operation was denied: ${denied.reason}`) }));
907
1153
  const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
908
1154
  const maintenance = yield* ConversationMaintenance;
909
1155
  const runtime = yield* DurableAgentRuntime;
910
- yield* maintenance.preArm;
911
- const intent = yield* runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
1156
+ const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
912
1157
  return ApprovalRecorded.make({ intent });
913
1158
  })), respond, Effect.flatMap(encodeResponse));
914
1159
  const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
915
1160
  const maintenance = yield* ConversationMaintenance;
916
1161
  const runtime = yield* DurableAgentRuntime;
917
- yield* maintenance.preArm;
918
- const intent = yield* runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
1162
+ const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
919
1163
  return UnknownResolutionRecorded.make({ intent });
920
1164
  })), respond, Effect.flatMap(encodeResponse));
921
1165
  /** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
@@ -983,8 +1227,7 @@ const verifyEndpoint = (encoded) => decodeAdminVerifyRequest(encoded).pipe(Effec
983
1227
  const retryEndpoint = (encoded) => decodeRetryCommand(encoded).pipe(Effect.mapError(protocolFailure("The retry command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
984
1228
  const maintenance = yield* ConversationMaintenance;
985
1229
  const runtime = yield* DurableAgentRuntime;
986
- yield* maintenance.preArm;
987
- const report = yield* runtime.retry(command);
1230
+ const report = yield* maintenance.withMutation(runtime.retry(command));
988
1231
  return RetryExecuted.make({ report });
989
1232
  })), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
990
1233
  const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pipe(Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")), Effect.flatMap((thresholds) => Effect.gen(function* () {
@@ -992,25 +1235,26 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
992
1235
  return ObligationsScanned.make({ report });
993
1236
  })), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
994
1237
  /**
995
- * Owner-side `portCall`: pre-arm before a mutating envelope (a routed admission committed by
996
- * THIS Object must already carry the alarm that will finish it), execute on the LOCAL facets
997
- * (never the routed decorators), then arm an immediate alarm so the mutated lane is
998
- * processed promptly. Protocol anomalies answer `PortFailed(PortProtocolError)`.
1238
+ * Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
1239
+ * public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
1240
+ * finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
1241
+ * alarm so the mutated lane is processed promptly. Protocol anomalies answer
1242
+ * `PortFailed(PortProtocolError)`.
999
1243
  */
1000
1244
  const portCallEndpoint = (encoded) => Effect.gen(function* () {
1001
1245
  const ports = yield* ConversationObjectPorts;
1002
1246
  const maintenance = yield* ConversationMaintenance;
1003
1247
  const alarm = yield* DurableAlarmService;
1004
1248
  const mutating = isMutatingPortRequest(encoded);
1005
- if (mutating) {
1006
- if ((yield* maintenance.preArm.pipe(Effect.exit))._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
1007
- }
1008
- const response = yield* ports.handle(encoded);
1249
+ const handled = yield* (mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)).pipe(Effect.exit);
1250
+ if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
1251
+ const response = handled.value;
1009
1252
  if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error)));
1010
1253
  return response;
1011
1254
  });
1012
1255
  const wakeEndpoint = Effect.gen(function* () {
1013
- yield* (yield* DurableAlarmService).scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ConversationObject.wake dropped", error)));
1256
+ const identity = yield* ConversationObjectIdentity;
1257
+ yield* (yield* WakeScheduler).notify(identity.conversationId);
1014
1258
  });
1015
1259
  const alarmEndpoint = Effect.gen(function* () {
1016
1260
  yield* (yield* ConversationMaintenance).pass;
@@ -1059,6 +1303,8 @@ const makeConversationObjectClass = (options, observability) => {
1059
1303
  const rpc = {
1060
1304
  submitEncoded: (encoded) => submitEndpoint(encoded),
1061
1305
  awaitSettlementEncoded: (encoded) => awaitSettlementEndpoint(encoded),
1306
+ awaitProgressEncoded: (encoded) => awaitProgressEndpoint(encoded),
1307
+ cancelProgressEncoded: (encoded) => cancelProgressEndpoint(encoded),
1062
1308
  observePage: (encoded) => observePageEndpoint(encoded),
1063
1309
  abortEncoded: (encoded) => abortEndpoint(encoded),
1064
1310
  resolveApprovalEncoded: (encoded) => resolveApprovalEndpoint(encoded),
@@ -1711,6 +1957,6 @@ const classifyWorkerFailure = (cause, maxWallTime) => {
1711
1957
  /** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
1712
1958
  const dynamicWorkerCodeExecutorLayer = (options) => Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
1713
1959
  //#endregion
1714
- export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareAdmissionLimitsValue, CloudflareBindingError, CloudflareConversationClient, CloudflareDurableRuntime, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError, CodeModeHostEntrypoint, ConversationClientError, ConversationMaintenance, ConversationObjectIdentity, ConversationObjectNamespace, ConversationObjectPorts, DEFAULT_MAX_DATABASE_BYTES, DurableAlarmError, DurableAlarmService, DurableObjectContext, ExplainedRecovery, HostFailed, HostFailure, HostProtocolError, HostResponse, MaintenancePassReport, ObligationsScanned, ObservePageRequest, ObservedPage, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
1960
+ export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, AwaitProgressRequest, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CancelProgressRequest, CloudflareAdmissionLimitsValue, CloudflareBindingError, CloudflareConversationClient, CloudflareDurableRuntime, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError, CodeModeHostEntrypoint, ConversationClientError, ConversationMaintenance, ConversationMaintenanceFailpoint, ConversationObjectIdentity, ConversationObjectNamespace, ConversationObjectPorts, DEFAULT_MAX_DATABASE_BYTES, DurableAlarmError, DurableAlarmService, DurableObjectContext, ExplainedRecovery, HostFailed, HostFailure, HostProtocolError, HostResponse, MaintenancePassReport, ObligationsScanned, ObservePageRequest, ObservedPage, ProgressCancelled, ProgressObserved, ProgressWaitRegistry, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeAwaitProgressRequest, decodeCancelProgressRequest, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeAwaitProgressRequest, encodeCancelProgressRequest, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
1715
1961
 
1716
1962
  //# sourceMappingURL=index.mjs.map