@effect-agent/platform-cloudflare 0.1.0-beta.44 → 0.1.0-beta.45

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/src/alarm.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- type ResolvedBinding,
3
2
  DurableAgentRuntime,
4
3
  SubmissionLedger,
5
4
  type DurableBindingFailure,
@@ -310,9 +309,7 @@ export class ThreadMaintenance extends Context.Service<
310
309
  ) => Effect.Effect<A, E | DurableAlarmError, R>;
311
310
  }
312
311
  >()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
313
- static readonly layer = (
314
- bindings: ReadonlyArray<ResolvedBinding>,
315
- ): Layer.Layer<
312
+ static readonly layer: Layer.Layer<
316
313
  ThreadMaintenance,
317
314
  never,
318
315
  | DurableAgentRuntime
@@ -322,288 +319,285 @@ export class ThreadMaintenance extends Context.Service<
322
319
  | CloudflareDurableRuntimeConfig
323
320
  | ThreadObjectIdentity
324
321
  | DurableObjectContext
325
- > =>
326
- Layer.effect(ThreadMaintenance)(
327
- Effect.gen(function* () {
328
- const runtime = yield* DurableAgentRuntime;
329
- const ledger = yield* SubmissionLedger;
330
- const alarm = yield* DurableAlarmService;
331
- const config = yield* CloudflareDurableRuntimeConfig;
332
- const identity = yield* ThreadObjectIdentity;
333
- const { ctx } = yield* DurableObjectContext;
334
- const failpoint = yield* ThreadMaintenanceFailpoint;
335
-
336
- /**
337
- * Consecutive no-progress passes an in-memory CACHE, not state: a fresh incarnation
338
- * restarts at zero and merely re-arms sooner than a long-lived one would have.
339
- */
340
- const stalls = yield* Ref.make(0);
341
- /**
342
- * Incarnation-local mutation count guarded with the generation transactions below. It is
343
- * deliberately not durable: after eviction every begun mutation has stopped, while its
344
- * pre-armed dirty generation remains durable for recovery. The short gate never spans the
345
- * caller's mutation or cross-Object I/O.
346
- */
347
- const activeMutations = yield* Ref.make(0);
348
- const generationGate = yield* Semaphore.make(1);
349
- // At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise
350
- // acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not
351
- // take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.
352
- const maintenancePassGate = yield* Semaphore.make(1);
353
- const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
354
-
355
- const runTransaction = <A>(
356
- operation: string,
357
- transaction: () => Promise<A>,
358
- ): Effect.Effect<A, DurableAlarmError> =>
359
- Effect.tryPromise({
360
- try: transaction,
361
- catch: alarmFailure(operation),
362
- });
363
-
364
- const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
365
- yield* failpoint.hit("maintenance:dirty:before");
366
- const now = yield* Clock.currentTimeMillis;
367
-
368
- yield* runTransaction("advance maintenance generation", () =>
369
- ctx.storage.transaction(async (transaction) => {
370
- const { state } = await readMaintenanceState(transaction);
371
-
372
- const next = ThreadMaintenanceState.make({
373
- ...state,
374
- dirty: state.dirty + 1n,
375
- });
376
-
377
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
378
- // The earliest configured retry bounds a newly actionable mutation without relying
379
- // on its best-effort immediate wake hint.
380
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
381
- }),
382
- );
383
- yield* failpoint.hit("maintenance:dirty:after");
384
- yield* Ref.update(activeMutations, (active) => active + 1);
322
+ > = Layer.effect(ThreadMaintenance)(
323
+ Effect.gen(function* () {
324
+ const runtime = yield* DurableAgentRuntime;
325
+ const ledger = yield* SubmissionLedger;
326
+ const alarm = yield* DurableAlarmService;
327
+ const config = yield* CloudflareDurableRuntimeConfig;
328
+ const identity = yield* ThreadObjectIdentity;
329
+ const { ctx } = yield* DurableObjectContext;
330
+ const failpoint = yield* ThreadMaintenanceFailpoint;
331
+
332
+ /**
333
+ * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
334
+ * restarts at zero and merely re-arms sooner than a long-lived one would have.
335
+ */
336
+ const stalls = yield* Ref.make(0);
337
+ /**
338
+ * Incarnation-local mutation count guarded with the generation transactions below. It is
339
+ * deliberately not durable: after eviction every begun mutation has stopped, while its
340
+ * pre-armed dirty generation remains durable for recovery. The short gate never spans the
341
+ * caller's mutation or cross-Object I/O.
342
+ */
343
+ const activeMutations = yield* Ref.make(0);
344
+ const generationGate = yield* Semaphore.make(1);
345
+ // At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise
346
+ // acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not
347
+ // take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.
348
+ const maintenancePassGate = yield* Semaphore.make(1);
349
+ const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
350
+
351
+ const runTransaction = <A>(
352
+ operation: string,
353
+ transaction: () => Promise<A>,
354
+ ): Effect.Effect<A, DurableAlarmError> =>
355
+ Effect.tryPromise({
356
+ try: transaction,
357
+ catch: alarmFailure(operation),
385
358
  });
386
359
 
387
- const endMutation = generationGate.withPermit(
388
- Ref.update(activeMutations, (active) => Math.max(0, active - 1)),
389
- );
360
+ const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
361
+ yield* failpoint.hit("maintenance:dirty:before");
362
+ const now = yield* Clock.currentTimeMillis;
390
363
 
391
- const withMutation = <A, E, R>(
392
- body: Effect.Effect<A, E, R>,
393
- ): Effect.Effect<A, E | DurableAlarmError, R> =>
394
- Effect.acquireUseRelease(
395
- generationGate.withPermit(beginMutation()),
396
- () =>
397
- failpoint.hit("maintenance:mutation:armed").pipe(
398
- Effect.andThen(body),
399
- Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
400
- ),
401
- () => endMutation,
402
- );
364
+ yield* runTransaction("advance maintenance generation", () =>
365
+ ctx.storage.transaction(async (transaction) => {
366
+ const { state } = await readMaintenanceState(transaction);
403
367
 
404
- const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
405
- yield* failpoint.hit("maintenance:ensure:before");
406
- const now = yield* Clock.currentTimeMillis;
368
+ const next = ThreadMaintenanceState.make({
369
+ ...state,
370
+ dirty: state.dirty + 1n,
371
+ });
407
372
 
408
- yield* runTransaction("ensure maintenance alarm", () =>
409
- ctx.storage.transaction(async (transaction) => {
410
- const { state, initialized } = await readMaintenanceState(transaction);
373
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
374
+ // The earliest configured retry bounds a newly actionable mutation without relying
375
+ // on its best-effort immediate wake hint.
376
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
377
+ }),
378
+ );
379
+ yield* failpoint.hit("maintenance:dirty:after");
380
+ yield* Ref.update(activeMutations, (active) => active + 1);
381
+ });
382
+
383
+ const endMutation = generationGate.withPermit(
384
+ Ref.update(activeMutations, (active) => Math.max(0, active - 1)),
385
+ );
386
+
387
+ const withMutation = <A, E, R>(
388
+ body: Effect.Effect<A, E, R>,
389
+ ): Effect.Effect<A, E | DurableAlarmError, R> =>
390
+ Effect.acquireUseRelease(
391
+ generationGate.withPermit(beginMutation()),
392
+ () =>
393
+ failpoint.hit("maintenance:mutation:armed").pipe(
394
+ Effect.andThen(body),
395
+ Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
396
+ ),
397
+ () => endMutation,
398
+ );
411
399
 
412
- if (!initialized) {
413
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
414
- }
415
- if (state.dirty > state.processed) {
416
- await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
417
- }
418
- }),
419
- );
420
- yield* failpoint.hit("maintenance:ensure:after");
421
- });
400
+ const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
401
+ yield* failpoint.hit("maintenance:ensure:before");
402
+ const now = yield* Clock.currentTimeMillis;
403
+
404
+ yield* runTransaction("ensure maintenance alarm", () =>
405
+ ctx.storage.transaction(async (transaction) => {
406
+ const { state, initialized } = await readMaintenanceState(transaction);
407
+
408
+ if (!initialized) {
409
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
410
+ }
411
+ if (state.dirty > state.processed) {
412
+ await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
413
+ }
414
+ }),
415
+ );
416
+ yield* failpoint.hit("maintenance:ensure:after");
417
+ });
418
+
419
+ const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
420
+ yield* failpoint.hit("maintenance:begin:before");
421
+ const now = yield* Clock.currentTimeMillis;
422
+
423
+ const result = yield* runTransaction("begin maintenance pass", () =>
424
+ ctx.storage.transaction(async (transaction) => {
425
+ const { state, initialized } = await readMaintenanceState(transaction);
426
+
427
+ if (!initialized) {
428
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
429
+ }
430
+ if (state.processed >= state.dirty) {
431
+ await transaction.deleteAlarm();
432
+
433
+ return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
434
+ }
435
+ // Pre-arm the earliest retry before recovery. A successful finish may move this slot
436
+ // LATER to its bounded backoff, which does not cancel the running handler.
437
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
438
+
439
+ return { _tag: "Actionable" as const, generation: state.dirty };
440
+ }),
441
+ );
422
442
 
423
- const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
424
- yield* failpoint.hit("maintenance:begin:before");
425
- const now = yield* Clock.currentTimeMillis;
443
+ yield* failpoint.hit("maintenance:begin:after");
426
444
 
427
- const result = yield* runTransaction("begin maintenance pass", () =>
428
- ctx.storage.transaction(async (transaction) => {
429
- const { state, initialized } = await readMaintenanceState(transaction);
445
+ return result;
446
+ });
430
447
 
431
- if (!initialized) {
432
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
433
- }
434
- if (state.processed >= state.dirty) {
435
- await transaction.deleteAlarm();
448
+ const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed: boolean) {
449
+ const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
450
+ progressed ? 0 : count + 1,
451
+ );
436
452
 
437
- return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
438
- }
439
- // Pre-arm the earliest retry before recovery. A successful finish may move this slot
440
- // LATER to its bounded backoff, which does not cancel the running handler.
441
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
453
+ if (progressed) return config.alarmBackoffBase;
454
+ const exponent = Math.min(priorStalls, 30);
455
+ const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
456
+ const jitter = yield* Random.next;
457
+ // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
458
+ // waiting longer than the deterministic bound.
459
+ const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
460
+
461
+ return Math.min(jittered, config.wakeScanInterval);
462
+ });
463
+
464
+ const pass = Effect.fn("ThreadMaintenance.pass")(function* (): Effect.fn.Return<
465
+ MaintenancePassReport,
466
+ MaintenancePassFailure
467
+ > {
468
+ const annotate = (report: MaintenancePassReport) =>
469
+ Effect.annotateCurrentSpan({
470
+ phase: report.phase,
471
+ recovered: report.recovered,
472
+ settled: report.settled,
473
+ nonterminal: report.nonterminal,
474
+ alarm: report.alarm,
475
+ }).pipe(Effect.as(report));
476
+
477
+ const started = yield* generationGate.withPermit(
478
+ Effect.gen(function* () {
479
+ const activeAtStart = yield* Ref.get(activeMutations);
480
+ const generation = yield* beginPass();
481
+
482
+ return { ...generation, activeAtStart };
483
+ }),
484
+ );
442
485
 
443
- return { _tag: "Actionable" as const, generation: state.dirty };
486
+ if (started._tag === "CaughtUp") {
487
+ return yield* annotate(
488
+ MaintenancePassReport.make({
489
+ phase: "caught-up",
490
+ recovered: 0,
491
+ settled: 0,
492
+ nonterminal: started.nonterminal,
493
+ alarm: "cleared",
444
494
  }),
445
495
  );
446
-
447
- yield* failpoint.hit("maintenance:begin:after");
448
-
449
- return result;
496
+ }
497
+ // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
498
+ const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
499
+ // Step 3 — one bounded drain pass over this Object's own lane.
500
+ const settlements = yield* runtime.processThreadResolved(identity.threadId);
501
+ // Observe residual state before acknowledging this exact pass-start generation.
502
+ const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
503
+ const reports = new Map(recovered.map((report) => [report.submissionId, report]));
504
+ const head = remaining[0];
505
+ const headWaiting = head !== undefined && stableExternalWait(head, reports);
506
+
507
+ const autonomous = remaining.some((snapshot, index) => {
508
+ // FIFO followers cannot execute through a stable external wait. Only plain queued
509
+ // input is dormant here; admission repairs and accepted aborts still need a pass.
510
+ if (
511
+ index > 0 &&
512
+ headWaiting &&
513
+ snapshot.state === "ready" &&
514
+ reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
515
+ )
516
+ return false;
517
+
518
+ return !stableExternalWait(snapshot, reports);
450
519
  });
451
520
 
452
- const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (
453
- progressed: boolean,
454
- ) {
455
- const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
456
- progressed ? 0 : count + 1,
457
- );
521
+ const progressed =
522
+ settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
458
523
 
459
- if (progressed) return config.alarmBackoffBase;
460
- const exponent = Math.min(priorStalls, 30);
461
- const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
462
- const jitter = yield* Random.next;
463
- // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
464
- // waiting longer than the deterministic bound.
465
- const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
524
+ const delay = autonomous ? yield* rearmDelay(progressed) : 0;
525
+ const now = yield* Clock.currentTimeMillis;
466
526
 
467
- return Math.min(jittered, config.wakeScanInterval);
468
- });
527
+ yield* failpoint.hit("maintenance:finish:before");
469
528
 
470
- const pass = Effect.fn("ThreadMaintenance.pass")(function* (): Effect.fn.Return<
471
- MaintenancePassReport,
472
- MaintenancePassFailure
473
- > {
474
- const annotate = (report: MaintenancePassReport) =>
475
- Effect.annotateCurrentSpan({
476
- phase: report.phase,
477
- recovered: report.recovered,
478
- settled: report.settled,
479
- nonterminal: report.nonterminal,
480
- alarm: report.alarm,
481
- }).pipe(Effect.as(report));
482
-
483
- const started = yield* generationGate.withPermit(
484
- Effect.gen(function* () {
485
- const activeAtStart = yield* Ref.get(activeMutations);
486
- const generation = yield* beginPass();
487
-
488
- return { ...generation, activeAtStart };
489
- }),
490
- );
529
+ const alarmDisposition = yield* generationGate.withPermit(
530
+ Effect.gen(function* () {
531
+ const active = yield* Ref.get(activeMutations);
491
532
 
492
- if (started._tag === "CaughtUp") {
493
- return yield* annotate(
494
- MaintenancePassReport.make({
495
- phase: "caught-up",
496
- recovered: 0,
497
- settled: 0,
498
- nonterminal: started.nonterminal,
499
- alarm: "cleared",
500
- }),
501
- );
502
- }
503
- // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
504
- const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
505
- // Step 3 — one bounded drain pass over this Object's own lane.
506
- const settlements = yield* runtime.processThreadResolved(identity.threadId, bindings);
507
- // Observe residual state before acknowledging this exact pass-start generation.
508
- const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
509
- const reports = new Map(recovered.map((report) => [report.submissionId, report]));
510
- const head = remaining[0];
511
- const headWaiting = head !== undefined && stableExternalWait(head, reports);
512
-
513
- const autonomous = remaining.some((snapshot, index) => {
514
- // FIFO followers cannot execute through a stable external wait. Only plain queued
515
- // input is dormant here; admission repairs and accepted aborts still need a pass.
516
- if (
517
- index > 0 &&
518
- headWaiting &&
519
- snapshot.state === "ready" &&
520
- reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
521
- )
522
- return false;
523
-
524
- return !stableExternalWait(snapshot, reports);
525
- });
526
-
527
- const progressed =
528
- settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
529
-
530
- const delay = autonomous ? yield* rearmDelay(progressed) : 0;
531
- const now = yield* Clock.currentTimeMillis;
532
-
533
- yield* failpoint.hit("maintenance:finish:before");
533
+ return yield* runTransaction("finish maintenance pass", () =>
534
+ ctx.storage.transaction(async (transaction) => {
535
+ const { state } = await readMaintenanceState(transaction);
534
536
 
535
- const alarmDisposition = yield* generationGate.withPermit(
536
- Effect.gen(function* () {
537
- const active = yield* Ref.get(activeMutations);
538
-
539
- return yield* runTransaction("finish maintenance pass", () =>
540
- ctx.storage.transaction(async (transaction) => {
541
- const { state } = await readMaintenanceState(transaction);
542
-
543
- // Autonomous work and in-flight mutations intentionally leave the observed
544
- // generation dirty. Otherwise acknowledge only the pass-start generation.
545
- const processed =
546
- autonomous || started.activeAtStart > 0 || active > 0
537
+ // Autonomous work and in-flight mutations intentionally leave the observed
538
+ // generation dirty. Otherwise acknowledge only the pass-start generation.
539
+ const processed =
540
+ autonomous || started.activeAtStart > 0 || active > 0
541
+ ? state.processed
542
+ : state.processed > started.generation
547
543
  ? state.processed
548
- : state.processed > started.generation
549
- ? state.processed
550
- : started.generation;
551
-
552
- const next = ThreadMaintenanceState.make({
553
- ...state,
554
- processed,
555
- nonterminal: remaining.length,
556
- });
557
-
558
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
559
- if (autonomous) {
560
- // Replace the crash-fallback slot with this pass's bounded backoff. The target
561
- // is never earlier than the begin-pass fallback, so workerd does not cancel
562
- // this running alarm handler before its report/span can complete.
563
- await transaction.setAlarm(now + delay);
564
-
565
- return "rearmed" as const;
566
- }
567
- if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
568
- // A mutation overlapped this pass's observation window or raced
569
- // acknowledgement. It stays dirty and its pre-armed bounded alarm survives;
570
- // unseen effects are never acknowledged. Do not accelerate that future alarm
571
- // from inside the current handler: workerd cancels a running handler when it
572
- // writes an earlier slot.
573
- await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
574
-
575
- return "rearmed" as const;
576
- }
577
- await transaction.deleteAlarm();
578
-
579
- return "cleared" as const;
580
- }),
581
- );
582
- }),
583
- );
584
-
585
- yield* failpoint.hit("maintenance:finish:after");
586
- if (alarmDisposition === "cleared") {
587
- yield* Ref.set(stalls, 0);
588
- }
544
+ : started.generation;
545
+
546
+ const next = ThreadMaintenanceState.make({
547
+ ...state,
548
+ processed,
549
+ nonterminal: remaining.length,
550
+ });
551
+
552
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
553
+ if (autonomous) {
554
+ // Replace the crash-fallback slot with this pass's bounded backoff. The target
555
+ // is never earlier than the begin-pass fallback, so workerd does not cancel
556
+ // this running alarm handler before its report/span can complete.
557
+ await transaction.setAlarm(now + delay);
558
+
559
+ return "rearmed" as const;
560
+ }
561
+ if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
562
+ // A mutation overlapped this pass's observation window or raced
563
+ // acknowledgement. It stays dirty and its pre-armed bounded alarm survives;
564
+ // unseen effects are never acknowledged. Do not accelerate that future alarm
565
+ // from inside the current handler: workerd cancels a running handler when it
566
+ // writes an earlier slot.
567
+ await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
568
+
569
+ return "rearmed" as const;
570
+ }
571
+ await transaction.deleteAlarm();
589
572
 
590
- return yield* annotate(
591
- MaintenancePassReport.make({
592
- phase: "actionable",
593
- recovered: recovered.length,
594
- settled: settlements.length,
595
- nonterminal: remaining.length,
596
- alarm: alarmDisposition,
597
- }),
598
- );
599
- });
573
+ return "cleared" as const;
574
+ }),
575
+ );
576
+ }),
577
+ );
600
578
 
601
- return ThreadMaintenance.of({
602
- // A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.
603
- pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
604
- ensureAlarm: ensureAlarm(),
605
- withMutation,
606
- });
607
- }),
608
- );
579
+ yield* failpoint.hit("maintenance:finish:after");
580
+ if (alarmDisposition === "cleared") {
581
+ yield* Ref.set(stalls, 0);
582
+ }
583
+
584
+ return yield* annotate(
585
+ MaintenancePassReport.make({
586
+ phase: "actionable",
587
+ recovered: recovered.length,
588
+ settled: settlements.length,
589
+ nonterminal: remaining.length,
590
+ alarm: alarmDisposition,
591
+ }),
592
+ );
593
+ });
594
+
595
+ return ThreadMaintenance.of({
596
+ // A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.
597
+ pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
598
+ ensureAlarm: ensureAlarm(),
599
+ withMutation,
600
+ });
601
+ }),
602
+ );
609
603
  }
package/src/layers.ts CHANGED
@@ -70,7 +70,7 @@ import { cloudflareWakeSchedulerLayer } from "./wake-scheduler.ts";
70
70
 
71
71
  /**
72
72
  * Raw (unvalidated) construction options for `ThreadObject.make`, mirroring
73
- * `NodeDurableRuntimeOptions`. Optional fields default to the documented production values
73
+ * `NodeDurableAgentRuntimeOptions`. Optional fields default to the documented production values
74
74
  * (`CLOUDFLARE_RUNTIME_DEFAULTS`); everything is schema-decoded into
75
75
  * `CloudflareDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
76
76
  */
@@ -367,7 +367,7 @@ export const layerFromBindings = (
367
367
 
368
368
  const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
369
369
 
370
- const runtimeStack = DurableAgentRuntime.layerWithServices.pipe(
370
+ const runtimeStack = DurableAgentRuntime.layerWithBindings(bindings).pipe(
371
371
  Layer.provideMerge(routedPorts),
372
372
  Layer.provideMerge(cloudflareWakeSchedulerLayer),
373
373
  Layer.provideMerge(base),
@@ -375,7 +375,7 @@ export const layerFromBindings = (
375
375
 
376
376
  return Layer.mergeAll(
377
377
  runtimeStack,
378
- ThreadMaintenance.layer(bindings).pipe(Layer.provide(runtimeStack)),
378
+ ThreadMaintenance.layer.pipe(Layer.provide(runtimeStack)),
379
379
  portsEndpointLayer,
380
380
  );
381
381
  }),