@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.15

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,4 +1,4 @@
1
- import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
1
+ import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Semaphore, Stream } from "effect";
2
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";
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";
@@ -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
  };
@@ -519,6 +641,7 @@ var CloudflareDurableRuntime = class {
519
641
  abortPollInterval: Duration.millis(config.abortPollInterval)
520
642
  }));
521
643
  const runtimeFailpointLayer = options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
644
+ const maintenanceFailpointLayer = options.maintenanceFailpoint === void 0 ? ConversationMaintenanceFailpoint.layer : Layer.succeed(ConversationMaintenanceFailpoint)({ hit: options.maintenanceFailpoint(ctx) });
522
645
  const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
523
646
  const bindingResolverLayer = Layer.effect(AgentBindingResolver)(Effect.map(resolveBindings(options.bindings, {
524
647
  ctx,
@@ -526,7 +649,7 @@ var CloudflareDurableRuntime = class {
526
649
  conversationId,
527
650
  producerId
528
651
  }), (bindings) => AgentBindingResolver.fromBindings(bindings)));
529
- const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer);
652
+ const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer, maintenanceFailpointLayer);
530
653
  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));
531
654
  return Layer.mergeAll(runtimeStack, ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)), portsEndpointLayer);
532
655
  }));
@@ -862,13 +985,12 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
862
985
  const maintenance = yield* ConversationMaintenance;
863
986
  const runtime = yield* DurableAgentRuntime;
864
987
  yield* gateAdmissionLimits(request);
865
- yield* maintenance.preArm;
866
- const receipt = yield* runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
988
+ const receipt = yield* maintenance.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
867
989
  conversationId: identity.conversationId,
868
990
  principal: request.principal,
869
991
  idempotencyKey: request.idempotencyKey,
870
992
  definitions: request.definitions
871
- });
993
+ }));
872
994
  return SubmitSucceeded.make({ receipt });
873
995
  })), respond, Effect.flatMap(encodeResponse));
874
996
  const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
@@ -892,8 +1014,7 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
892
1014
  const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
893
1015
  const maintenance = yield* ConversationMaintenance;
894
1016
  const runtime = yield* DurableAgentRuntime;
895
- yield* maintenance.preArm;
896
- const intent = yield* runtime.abort(command);
1017
+ const intent = yield* maintenance.withMutation(runtime.abort(command));
897
1018
  return AbortRecorded.make({ intent });
898
1019
  })), respond, Effect.flatMap(encodeResponse));
899
1020
  /**
@@ -907,15 +1028,13 @@ const deniedToProtocolFailure = (denied) => Effect.fail(HostProtocolError.make({
907
1028
  const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
908
1029
  const maintenance = yield* ConversationMaintenance;
909
1030
  const runtime = yield* DurableAgentRuntime;
910
- yield* maintenance.preArm;
911
- const intent = yield* runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
1031
+ const intent = yield* maintenance.withMutation(runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)));
912
1032
  return ApprovalRecorded.make({ intent });
913
1033
  })), respond, Effect.flatMap(encodeResponse));
914
1034
  const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
915
1035
  const maintenance = yield* ConversationMaintenance;
916
1036
  const runtime = yield* DurableAgentRuntime;
917
- yield* maintenance.preArm;
918
- const intent = yield* runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
1037
+ const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)));
919
1038
  return UnknownResolutionRecorded.make({ intent });
920
1039
  })), respond, Effect.flatMap(encodeResponse));
921
1040
  /** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
@@ -983,8 +1102,7 @@ const verifyEndpoint = (encoded) => decodeAdminVerifyRequest(encoded).pipe(Effec
983
1102
  const retryEndpoint = (encoded) => decodeRetryCommand(encoded).pipe(Effect.mapError(protocolFailure("The retry command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
984
1103
  const maintenance = yield* ConversationMaintenance;
985
1104
  const runtime = yield* DurableAgentRuntime;
986
- yield* maintenance.preArm;
987
- const report = yield* runtime.retry(command);
1105
+ const report = yield* maintenance.withMutation(runtime.retry(command));
988
1106
  return RetryExecuted.make({ report });
989
1107
  })), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
990
1108
  const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pipe(Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")), Effect.flatMap((thresholds) => Effect.gen(function* () {
@@ -992,20 +1110,20 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
992
1110
  return ObligationsScanned.make({ report });
993
1111
  })), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
994
1112
  /**
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)`.
1113
+ * Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
1114
+ * public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
1115
+ * finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
1116
+ * alarm so the mutated lane is processed promptly. Protocol anomalies answer
1117
+ * `PortFailed(PortProtocolError)`.
999
1118
  */
1000
1119
  const portCallEndpoint = (encoded) => Effect.gen(function* () {
1001
1120
  const ports = yield* ConversationObjectPorts;
1002
1121
  const maintenance = yield* ConversationMaintenance;
1003
1122
  const alarm = yield* DurableAlarmService;
1004
1123
  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);
1124
+ const handled = yield* (mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)).pipe(Effect.exit);
1125
+ if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
1126
+ const response = handled.value;
1009
1127
  if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error)));
1010
1128
  return response;
1011
1129
  });
@@ -1711,6 +1829,6 @@ const classifyWorkerFailure = (cause, maxWallTime) => {
1711
1829
  /** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
1712
1830
  const dynamicWorkerCodeExecutorLayer = (options) => Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
1713
1831
  //#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 };
1832
+ 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, ConversationMaintenanceFailpoint, 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 };
1715
1833
 
1716
1834
  //# sourceMappingURL=index.mjs.map