@yielded/agent-platform-cloudflare 0.1.0-beta.167 → 0.1.0-beta.169

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/{Alarm-B0ODr1NE.mjs → Alarm-BMEAZo3p.mjs} +261 -159
  2. package/dist/Alarm-BMEAZo3p.mjs.map +1 -0
  3. package/dist/Alarm.d.mts +29 -36
  4. package/dist/Alarm.mjs +2 -2
  5. package/dist/BrowserCredentials-C5IWfi7f.d.mts +91 -0
  6. package/dist/BrowserCredentials.d.mts +2 -91
  7. package/dist/BrowserRestCapture.mjs +3 -3
  8. package/dist/BrowserRestCapture.mjs.map +1 -1
  9. package/dist/BrowserRestCrawl.mjs +6 -6
  10. package/dist/BrowserRestCrawl.mjs.map +1 -1
  11. package/dist/{BrowserSession-RfIzxGCH.mjs → BrowserSession-BfNmyayH.mjs} +5 -5
  12. package/dist/BrowserSession-BfNmyayH.mjs.map +1 -0
  13. package/dist/{BrowserSession-B969Xg-2.d.mts → BrowserSession-Bx0V1-6_.d.mts} +6 -3
  14. package/dist/BrowserSession.d.mts +1 -1
  15. package/dist/BrowserSession.mjs +2 -2
  16. package/dist/BrowserUse-Bc2JdLW2.mjs +1528 -0
  17. package/dist/BrowserUse-Bc2JdLW2.mjs.map +1 -0
  18. package/dist/BrowserUse.d.mts +83 -0
  19. package/dist/BrowserUse.mjs +2 -0
  20. package/dist/CloudflareBindings.d.mts +8 -1
  21. package/dist/CloudflareBindings.mjs +2 -2
  22. package/dist/CloudflareBindings.mjs.map +1 -1
  23. package/dist/{CloudflareBrowser-5an6m-Qb.mjs → CloudflareBrowser-Vt8qTdYW.mjs} +11 -13
  24. package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -0
  25. package/dist/CloudflareBrowser.mjs +1 -1
  26. package/dist/CloudflareCodeMode.mjs +1 -1
  27. package/dist/CloudflareCodeMode.mjs.map +1 -1
  28. package/dist/{CloudflareConfig-qDrRLofL.d.mts → CloudflareConfig-C9MvPbX8.d.mts} +9 -11
  29. package/dist/CloudflareConfig.d.mts +1 -1
  30. package/dist/CloudflareConfig.mjs +9 -12
  31. package/dist/CloudflareConfig.mjs.map +1 -1
  32. package/dist/CloudflareMemory.d.mts +3 -25
  33. package/dist/CloudflareMemory.mjs +33 -31
  34. package/dist/CloudflareMemory.mjs.map +1 -1
  35. package/dist/CloudflareScheduling.mjs +8 -15
  36. package/dist/CloudflareScheduling.mjs.map +1 -1
  37. package/dist/CloudflareSubscriptions.mjs +9 -16
  38. package/dist/CloudflareSubscriptions.mjs.map +1 -1
  39. package/dist/CloudflareThreadClient-CLZpN9zU.mjs +588 -0
  40. package/dist/CloudflareThreadClient-CLZpN9zU.mjs.map +1 -0
  41. package/dist/CloudflareThreadClient-QsVzLGGZ.d.mts +2033 -0
  42. package/dist/CloudflareThreadClient.d.mts +2 -1796
  43. package/dist/CloudflareThreadClient.mjs +2 -382
  44. package/dist/{InteractiveBrowser-BbG3kGA9.mjs → InteractiveBrowser-LdKzQeLR.mjs} +18 -19
  45. package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +1 -0
  46. package/dist/InteractiveBrowser.mjs +1 -1
  47. package/dist/{ThreadObject-BcfXbzSO.d.mts → ThreadObject-7s5XnlH-.d.mts} +103 -79
  48. package/dist/{ThreadObject-DHjrnlX4.mjs → ThreadObject-CgXZMlr4.mjs} +265 -66
  49. package/dist/ThreadObject-CgXZMlr4.mjs.map +1 -0
  50. package/dist/ThreadObject.d.mts +2 -2
  51. package/dist/ThreadObject.mjs +2 -3
  52. package/dist/WakeScheduler.d.mts +6 -5
  53. package/dist/WakeScheduler.mjs +15 -11
  54. package/dist/WakeScheduler.mjs.map +1 -1
  55. package/dist/index.d.mts +7 -6
  56. package/dist/index.mjs +7 -6
  57. package/dist/page-function-CmUCcyNP.mjs +11 -0
  58. package/dist/page-function-CmUCcyNP.mjs.map +1 -0
  59. package/dist/{prepared-admission-DMX5p9dC.mjs → prepared-admission-B61A7y6O.mjs} +3 -3
  60. package/dist/{prepared-admission-DMX5p9dC.mjs.map → prepared-admission-B61A7y6O.mjs.map} +1 -1
  61. package/package.json +1 -1
  62. package/src/Alarm.ts +512 -279
  63. package/src/BrowserRestCapture.ts +2 -2
  64. package/src/BrowserRestCrawl.ts +10 -14
  65. package/src/BrowserSession.ts +10 -2
  66. package/src/BrowserUse.ts +1591 -0
  67. package/src/CloudflareBindings.ts +13 -4
  68. package/src/CloudflareCodeMode.ts +1 -1
  69. package/src/CloudflareConfig.ts +9 -12
  70. package/src/CloudflareMemory.ts +21 -28
  71. package/src/CloudflareScheduling.ts +7 -15
  72. package/src/CloudflareSubscriptions.ts +11 -20
  73. package/src/CloudflareThreadClient.ts +289 -9
  74. package/src/InteractiveBrowser.ts +325 -323
  75. package/src/ThreadObject.ts +172 -52
  76. package/src/WakeScheduler.ts +29 -18
  77. package/src/index.ts +1 -0
  78. package/src/internal/browser-dom.ts +886 -0
  79. package/src/internal/browser-file-selection.ts +8 -1
  80. package/src/internal/browser-jev-dom.ts +362 -0
  81. package/src/internal/browser-quick-action.ts +7 -8
  82. package/src/internal/browser-session-lifecycle.ts +2 -5
  83. package/src/internal/crypto.ts +41 -0
  84. package/src/internal/due-queue.ts +5 -69
  85. package/src/internal/layers.ts +100 -17
  86. package/src/internal/live-text-protocol.ts +51 -0
  87. package/src/internal/live-text.ts +256 -0
  88. package/src/internal/page-function.ts +12 -0
  89. package/src/internal/prepared-admission.ts +1 -1
  90. package/src/internal/progress-wait.ts +19 -24
  91. package/src/internal/transport.ts +1 -1
  92. package/dist/Alarm-B0ODr1NE.mjs.map +0 -1
  93. package/dist/BrowserSession-RfIzxGCH.mjs.map +0 -1
  94. package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +0 -1
  95. package/dist/CloudflareThreadClient.mjs.map +0 -1
  96. package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +0 -1
  97. package/dist/ThreadObject-DHjrnlX4.mjs.map +0 -1
@@ -2,6 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { DurableObjectContext } from "./CloudflareBindings.mjs";
3
3
  import { AuxiliaryDispatchMillis, CloudflareDurableRuntimeConfig } from "./CloudflareConfig.mjs";
4
4
  import { r as safeCauseMessage } from "./boundary-BguazVkh.mjs";
5
+ import { CurrentNativeSource } from "@yielded/agent-storage-cloudflare/port-routing";
5
6
  import "@yielded/agent/agent-registration";
6
7
  import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryBlocked, RecoveryFailure, RecoverySweepResult, isolateRecovery } from "@yielded/agent/durable-agent-runtime";
7
8
  import { SubmissionId, ThreadId } from "@yielded/agent/identifiers";
@@ -9,7 +10,7 @@ import { AbortIntentRequest, SubmissionLedger } from "@yielded/agent/submission-
9
10
  import { ThreadProjectionMaintenance } from "@yielded/agent/thread-projection-maintenance";
10
11
  import { WakeScheduler } from "@yielded/agent/wake-scheduler";
11
12
  import { Cause, Clock, Context, DateTime, Deferred, Effect, ErrorReporter, Exit, Fiber, Layer, Option, Random, Ref, Result, Scheduler, Schema, Scope, Semaphore, Stream, Struct } from "effect";
12
- import { DurableObjectStorage } from "effect-cf";
13
+ import { DurableObjectAlarm, DurableObjectStorage } from "effect-cf";
13
14
  import { SqlClient } from "effect/sql/SqlClient";
14
15
  import { SqlError, UnknownError } from "effect/sql/SqlError";
15
16
  //#region src/internal/due-queue.ts
@@ -134,34 +135,6 @@ const make = (storage) => {
134
135
  const changes = [...current.changes.values()].filter(({ before, after }) => before === void 0 || !equivalent(before, after));
135
136
  for (let offset = 0; offset < changes.length; offset += 11) write(changes.slice(offset, offset + 11));
136
137
  };
137
- /** Own the flush inside the source transaction without changing the SQL service.
138
- * Acquire its shared connection permit through Effect requirements. Cleanup follows
139
- * native settlement, including commit rejection or interruption. */
140
- const withTransaction = (body) => Effect.flatMap(SqlClient, (client) => Effect.flatMap(Effect.serviceOption(client.transactionService), (ambient) => {
141
- if (ambient._tag === "Some" && !ownsTransaction(storage, ambient.value[1])) return Effect.fail(SqlError.make({ reason: UnknownError.make({
142
- operation: "begin maintenance source transaction",
143
- message: "Use ThreadMutationGate.withTransaction at the outermost source transaction",
144
- cause: /* @__PURE__ */ new Error("Unwrapped SQL transaction cannot own buffered maintenance intent")
145
- }) }));
146
- return Effect.uninterruptibleMask((restore) => Effect.suspend(() => {
147
- let frame;
148
- return client.withTransaction(Effect.flatMap(Effect.serviceOption(client.transactionService), (transaction) => Effect.sync(() => {
149
- if (transaction._tag === "None") throw new Error("SQL transaction unavailable");
150
- frame = begin(transaction.value[1]);
151
- })).pipe(Effect.andThen(restore(body)), Effect.tap(() => Effect.try({
152
- try: () => {
153
- if (frame !== void 0) flush(frame);
154
- },
155
- catch: (cause) => SqlError.make({ reason: UnknownError.make({
156
- cause,
157
- operation: "flush maintenance due queue",
158
- message: "Maintenance intent could not commit"
159
- }) })
160
- })))).pipe(Effect.onExit((exit) => Effect.sync(() => {
161
- if (frame !== void 0) finish(frame, Exit.isSuccess(exit));
162
- })), Effect.provideService(Scheduler.PreventSchedulerYield, true));
163
- }));
164
- }));
165
138
  const transaction = async (body) => {
166
139
  let frame;
167
140
  try {
@@ -308,7 +281,9 @@ const make = (storage) => {
308
281
  return {
309
282
  initialize,
310
283
  withView,
311
- withTransaction,
284
+ begin,
285
+ flush,
286
+ finish,
312
287
  transaction,
313
288
  read,
314
289
  dirty,
@@ -329,6 +304,7 @@ var Alarm_exports = /* @__PURE__ */ __exportAll({
329
304
  MaintenancePassReport: () => MaintenancePassReport,
330
305
  ThreadHostMaintenance: () => ThreadHostMaintenance,
331
306
  ThreadMaintenance: () => ThreadMaintenance,
307
+ ThreadMaintenanceAlarm: () => ThreadMaintenanceAlarm,
332
308
  ThreadMaintenanceFailpoint: () => ThreadMaintenanceFailpoint,
333
309
  ThreadMessageDelivery: () => ThreadMessageDelivery,
334
310
  ThreadMutationGate: () => ThreadMutationGate,
@@ -339,10 +315,8 @@ var Alarm_exports = /* @__PURE__ */ __exportAll({
339
315
  publishCommitted: () => publishCommitted
340
316
  });
341
317
  /**
342
- * The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
343
- * alarm slot; due work (lease expiry, settlement
344
- * and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
345
- * the slot always holds the EARLIEST deadline any caller asked for.
318
+ * The maintenance queue owns lease expiry, settlement, retries and dirty generations.
319
+ * Its manual effect-cf alarm shares the native slot with other registered alarms.
346
320
  *
347
321
  * The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
348
322
  * maintenance generation and a committed alarm. Stable externally-driven waits may be
@@ -382,41 +356,123 @@ const makeStorageOperation = Effect.map(makeStorageEffect, (run) => (operation,
382
356
  try: execute,
383
357
  catch: alarmFailure(operation)
384
358
  })));
385
- /** Native `ctx.storage` alarm slot owned by ThreadMaintenance; storage is truth. */
359
+ /** One manual alarm for the entire consumer-owned maintenance queue. */
360
+ var ThreadMaintenanceAlarm = class extends DurableObjectAlarm.Tag()("@yielded/agent/thread-maintenance", { "@yielded/agent/thread-maintenance": {
361
+ payload: Schema.Null,
362
+ lifecycle: "manual"
363
+ } }) {};
364
+ const maintenanceAlarmRef = {
365
+ tag: "@yielded/agent/thread-maintenance",
366
+ id: "queue"
367
+ };
368
+ const maintenanceAlarmInput = (deadline) => ({
369
+ ...maintenanceAlarmRef,
370
+ payload: null,
371
+ runAt: DateTime.makeUnsafe(deadline)
372
+ });
373
+ /** Named maintenance checkpoint; the library owns the shared native alarm slot. */
386
374
  var DurableAlarmService = class DurableAlarmService extends Context.Service()("@effect-agent/platform-cloudflare/DurableAlarmService") {
387
375
  static layer = Layer.effect(DurableAlarmService)(Effect.gen(function* () {
376
+ const alarms = yield* ThreadMaintenanceAlarm;
388
377
  const { ctx } = yield* DurableObjectContext;
389
- /**
390
- * In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
391
- * running pass, and a deferred wake lost to eviction was only ever a promptness hint
392
- * on top of the already-committed pre-armed alarm.
393
- */
394
- const runningPasses = yield* Ref.make(0);
395
- const storageOperation = yield* makeStorageOperation;
396
- const scheduled = storageOperation("get alarm", () => ctx.storage.getAlarm()).pipe(Effect.map((deadline) => deadline === null ? Option.none() : Option.some(deadline)));
397
- const scheduleAt = (epochMillis) => storageOperation("set alarm", () => ctx.storage.setAlarm(epochMillis));
398
- const ensureScheduledBy = (epochMillis) => storageOperation("ensure alarm", () => ctx.storage.transaction(async (transaction) => {
399
- const existing = await transaction.getAlarm();
400
- if (existing === null || existing > epochMillis) await transaction.setAlarm(epochMillis);
401
- }));
402
- const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => storageOperation("wake maintenance lanes", () => ctx.storage.transaction(async (transaction) => {
403
- const next$7 = next(make(ctx.storage).read());
404
- if (Number.isFinite(next$7)) await ensureTransactionAlarmBy(transaction, Math.max(now, next$7));
405
- }))));
406
- const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
407
- const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
408
- const cancel = storageOperation("delete alarm", () => ctx.storage.deleteAlarm());
378
+ const run = yield* makeStorageEffect;
379
+ const inlineWork = yield* Ref.make(0);
380
+ const wrap = (operation, effect) => run(operation, effect.pipe(Effect.mapError(alarmFailure(operation))));
409
381
  return DurableAlarmService.of({
410
- scheduled,
411
- scheduleAt,
412
- ensureScheduledBy,
413
- scheduleNow,
414
- withWakesDeferred,
415
- cancel
382
+ scheduled: wrap("read maintenance checkpoint", alarms.getAlarmStatus(maintenanceAlarmRef)).pipe(Effect.map((at) => at === void 0 ? Option.none() : Option.some(DateTime.toEpochMillis(at.runAt)))),
383
+ scheduleAt: (at) => wrap("replace maintenance checkpoint", alarms.scheduleAlarm(maintenanceAlarmInput(at))),
384
+ ensureScheduledBy: (at) => wrap("advance maintenance checkpoint", alarms.scheduleAlarmEarlier(maintenanceAlarmInput(at))),
385
+ scheduleNow: Ref.get(inlineWork).pipe(Effect.flatMap((active) => active > 0 ? Effect.void : wrap("hint maintenance checkpoint", alarms.transaction((tx) => Effect.gen(function* () {
386
+ const now = yield* Clock.currentTimeMillis;
387
+ const next$4 = next(make(ctx.storage).read());
388
+ if (Number.isFinite(next$4)) yield* tx.scheduleAlarmEarlier(maintenanceAlarmInput(Math.max(now, next$4)));
389
+ }))))),
390
+ withHintsDeferred: (body) => Effect.acquireUseRelease(Ref.update(inlineWork, (active) => active + 1), () => body, () => Ref.update(inlineWork, (active) => active - 1)),
391
+ cancel: wrap("cancel maintenance checkpoint", alarms.cancelAlarm(maintenanceAlarmRef))
416
392
  });
417
393
  }));
418
394
  };
419
- /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
395
+ const CurrentCheckpoint = Context.Reference("@yielded/agent/cloudflare/CurrentCheckpoint", { defaultValue: () => void 0 });
396
+ /** One source transaction owns queue intent and the library checkpoint through commit. */
397
+ const makeAlarmOperation = Effect.gen(function* () {
398
+ const alarms = yield* ThreadMaintenanceAlarm;
399
+ const sql = yield* SqlClient;
400
+ const { ctx } = yield* DurableObjectContext;
401
+ const queue = make(ctx.storage);
402
+ const transaction = (body) => alarms.transaction((mutations) => Effect.withFiber((owner) => {
403
+ let active = true;
404
+ const requireActive = Effect.withFiber((fiber) => active && fiber === owner ? Effect.void : Effect.fail(DurableAlarmError.make({
405
+ operation: "enroll maintenance source",
406
+ message: "Source intent requires its active transaction callback's fiber"
407
+ })));
408
+ return body.pipe(Effect.provideService(CurrentCheckpoint, {
409
+ mutations,
410
+ requireActive
411
+ }), Effect.ensuring(Effect.sync(() => {
412
+ active = false;
413
+ })));
414
+ }), { sqlClient: sql }).pipe(Effect.mapError((cause) => cause instanceof DurableObjectStorage.StorageOperationError ? SqlError.make({ reason: UnknownError.make({
415
+ operation: "commit maintenance checkpoint",
416
+ message: "Source facts and maintenance checkpoint could not commit",
417
+ cause
418
+ }) }) : cause));
419
+ const withTransaction = (body) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (ambient) => {
420
+ if (ambient._tag === "Some" && !ownsTransaction(ctx.storage, ambient.value[1])) return Effect.fail(SqlError.make({ reason: UnknownError.make({
421
+ operation: "begin maintenance source transaction",
422
+ message: "Use ThreadMutationGate.withTransaction at the outermost source transaction",
423
+ cause: /* @__PURE__ */ new Error("Unwrapped SQL transaction cannot own buffered maintenance intent")
424
+ }) }));
425
+ return Effect.uninterruptibleMask((restore) => Effect.suspend(() => {
426
+ let frame;
427
+ return transaction(Effect.flatMap(Effect.serviceOption(sql.transactionService), (transaction) => Effect.sync(() => {
428
+ if (transaction._tag === "None") throw new Error("SQL transaction unavailable");
429
+ frame = queue.begin(transaction.value[1]);
430
+ })).pipe(Effect.andThen(restore(body)), Effect.tap(() => Effect.try({
431
+ try: () => {
432
+ if (frame !== void 0) queue.flush(frame);
433
+ },
434
+ catch: (cause) => SqlError.make({ reason: UnknownError.make({
435
+ cause,
436
+ operation: "flush maintenance due queue",
437
+ message: "Maintenance intent could not commit"
438
+ }) })
439
+ })))).pipe(Effect.onExit((exit) => Effect.sync(() => {
440
+ if (frame !== void 0) queue.finish(frame, Exit.isSuccess(exit));
441
+ })), Effect.provideService(Scheduler.PreventSchedulerYield, true));
442
+ }));
443
+ }).pipe(Effect.provideService(SqlClient, sql));
444
+ const run = (operation, execute) => {
445
+ const commit = Effect.flatMap(CurrentCheckpoint, (tx) => {
446
+ if (tx === void 0) return Effect.die(/* @__PURE__ */ new Error("Maintenance checkpoint transaction unavailable"));
447
+ return tx.requireActive.pipe(Effect.andThen(Effect.suspend(() => {
448
+ let checkpoint = Effect.void;
449
+ const intent = {
450
+ at: (at) => {
451
+ checkpoint = tx.mutations.scheduleAlarm(maintenanceAlarmInput(at));
452
+ },
453
+ earlier: (at) => {
454
+ checkpoint = tx.mutations.scheduleAlarmEarlier(maintenanceAlarmInput(at));
455
+ },
456
+ cancel: () => {
457
+ checkpoint = tx.mutations.cancelAlarm(maintenanceAlarmRef);
458
+ }
459
+ };
460
+ return Effect.tryPromise({
461
+ try: () => execute(ctx.storage, intent),
462
+ catch: alarmFailure(operation)
463
+ }).pipe(Effect.tap(() => checkpoint));
464
+ })));
465
+ });
466
+ return Effect.gen(function* () {
467
+ if ((yield* CurrentCheckpoint) !== void 0) return yield* commit;
468
+ return yield* (yield* Effect.serviceOption(sql.transactionService))._tag === "Some" && !buffering(ctx.storage) ? transaction(commit).pipe(Effect.ensuring(Effect.sync(() => invalidate(ctx.storage)))) : withTransaction(commit);
469
+ }).pipe(Effect.uninterruptible, Effect.mapError(alarmFailure(operation)));
470
+ };
471
+ return {
472
+ run,
473
+ withTransaction
474
+ };
475
+ });
420
476
  var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
421
477
  /** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
422
478
  phase: Schema.Literals(["caught-up", "actionable"]),
@@ -490,6 +546,8 @@ const decodeRecoveryEvent = Schema.decodeUnknownSync(ThreadRecoveryFaultEvent);
490
546
  * Object and sequence; callbacks may repeat after interruption. This is a trusted host hook,
491
547
  * not pre-authorized UI data: the host owns recipient authorization and safe presentation.
492
548
  * Capture services in the Layer. Do not call producers or write the raw alarm slot.
549
+ * Without this service, maintenance acknowledges and discards transitions. Install the
550
+ * handler before the first maintenance pass if the application needs that event stream.
493
551
  */
494
552
  var ThreadRecoveryEvents = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadRecoveryEvents") {};
495
553
  var BindingWait = class extends Schema.Class("BindingWait")({
@@ -549,9 +607,13 @@ const recoveryEvent = (fault, submissionId, transition, occurredAt) => ({
549
607
  firstFailedAt: fault.firstFailedAt,
550
608
  failure: fault.failure
551
609
  });
552
- const nativeDispatchConcurrency = 2;
553
610
  /** Post-native delivery shares a finite connection budget across independent host lanes. */
554
611
  const afterNativeDispatchConcurrency = 2;
612
+ const NativeWorkCursor = Schema.Struct({
613
+ threadId: ThreadId,
614
+ cursor: Schema.NonEmptyString.check(Schema.isMaxLength(8192))
615
+ });
616
+ const workCursorKey = (threadId) => `effect-agent:work-recovery-cursor:v1:${threadId}`;
555
617
  var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
556
618
  generation: MaintenanceGeneration,
557
619
  notBefore: Schema.Finite,
@@ -597,10 +659,6 @@ const readMaintenanceState = async (transaction) => {
597
659
  initialized: true
598
660
  };
599
661
  };
600
- const ensureTransactionAlarmBy = async (transaction, deadline) => {
601
- const scheduled = await transaction.getAlarm();
602
- if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
603
- };
604
662
  const stableExternalWait = (snapshot, reports) => {
605
663
  const report = reports.get(snapshot.submissionId);
606
664
  if (report?.decision._tag === "SettleAborted") return false;
@@ -612,9 +670,7 @@ const stableExternalWait = (snapshot, reports) => {
612
670
  case "input-applied":
613
671
  case "joining":
614
672
  case "ready":
615
- case "running":
616
- case "settled":
617
- case "terminalizing": return false;
673
+ case "settled": return false;
618
674
  }
619
675
  };
620
676
  /**
@@ -623,7 +679,6 @@ const stableExternalWait = (snapshot, reports) => {
623
679
  * Layers must reuse that instance; a second gate cannot observe the native producers' activity.
624
680
  */
625
681
  const CurrentMutationLanes = Context.Reference("@effect-agent/platform-cloudflare/CurrentMutationLanes", { defaultValue: () => [] });
626
- const CurrentNativeSource = Context.Reference("@effect-agent/platform-cloudflare/CurrentNativeSource", { defaultValue: () => false });
627
682
  var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@effect-agent/platform-cloudflare/internal/ThreadMutationGate") {
628
683
  static layer = Layer.effect(this)(Effect.gen(function* () {
629
684
  const { ctx } = yield* DurableObjectContext;
@@ -644,6 +699,7 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
644
699
  });
645
700
  const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
646
701
  const runTransaction = yield* makeStorageOperation;
702
+ const { run: runAlarmTransaction, withTransaction } = yield* makeAlarmOperation;
647
703
  const validateSourceBoundary = Effect.flatMap(Effect.serviceOption(sql.transactionService), (current) => current._tag === "Some" && buffering(ctx.storage) && !ownsTransaction(ctx.storage, current.value[1]) ? Effect.fail(DurableAlarmError.make({
648
704
  operation: "enroll maintenance source",
649
705
  message: "Nested source scheduling requires ThreadMutationGate.withTransaction"
@@ -652,11 +708,10 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
652
708
  dueQueue.initialize();
653
709
  dueQueue.register(Native);
654
710
  });
655
- const schedule = Effect.fn("ThreadMutationGate.schedule")(function* (id, dueAt, progressCursor) {
711
+ const schedule = Effect.fnUntraced(function* (id, dueAt, progressCursor) {
656
712
  yield* validateSourceBoundary;
657
713
  const now = yield* Clock.currentTimeMillis;
658
- const current = yield* Effect.serviceOption(sql.transactionService);
659
- const enroll = async (transaction) => {
714
+ const enroll = async (transaction, checkpoint) => {
660
715
  if (progressCursor === void 0) dueQueue.dirty(id, dueAt);
661
716
  else if (dueQueue.progress(id, dueAt, progressCursor) && (id === "effect-agent:native" || id === "effect-agent:publication")) {
662
717
  if (id === "effect-agent:publication") dueQueue.dirty(Native, dueAt, true);
@@ -666,32 +721,31 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
666
721
  dirty: state.dirty + 1n
667
722
  })));
668
723
  }
669
- const next$2 = next(dueQueue.read());
670
- if (Number.isFinite(next$2)) await ensureTransactionAlarmBy(transaction, Math.max(next$2, now + minimumAlarmDelay));
724
+ const next$6 = next(dueQueue.read());
725
+ if (Number.isFinite(next$6)) checkpoint.earlier(Math.max(next$6, now + minimumAlarmDelay));
671
726
  };
672
- yield* runTransaction("schedule maintenance lane", () => Option.isSome(current) ? enroll(ctx.storage) : dueQueue.transaction(enroll));
727
+ yield* runAlarmTransaction("schedule maintenance lane", enroll);
673
728
  yield* notify;
674
729
  });
675
- const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* (invalidatesRecovery, lanes) {
730
+ const beginMutation = Effect.fnUntraced(function* (invalidatesRecovery, lanes) {
676
731
  yield* failpoint.hit("maintenance:dirty:before");
677
732
  const now = yield* Clock.currentTimeMillis;
678
733
  const nativeSource = yield* CurrentNativeSource;
679
734
  const publicationRequired = yield* requiresPublication;
680
- if (invalidatesRecovery || lanes.length > 0) yield* runTransaction("advance maintenance generation", () => dueQueue.transaction(async (transaction) => {
735
+ if (invalidatesRecovery || lanes.length > 0) yield* runAlarmTransaction("advance maintenance generation", async (transaction, checkpoint) => {
681
736
  const { state, initialized } = await readMaintenanceState(transaction);
682
737
  if (invalidatesRecovery) {
683
738
  dueQueue.dirty(Native, now);
684
739
  if (publicationRequired) dueQueue.dirty(Publication, now);
685
740
  }
686
741
  for (const id of lanes) dueQueue.dirty(id, now, false);
687
- const next$1 = ThreadMaintenanceState.make({
742
+ if (!initialized || invalidatesRecovery && !nativeSource) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
688
743
  ...state,
689
744
  dirty: state.dirty + (invalidatesRecovery && !nativeSource ? 1n : 0n)
690
- });
691
- if (invalidatesRecovery || !initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next$1));
745
+ })));
692
746
  const deadline = next(dueQueue.read());
693
- if (Number.isFinite(deadline)) await ensureTransactionAlarmBy(transaction, Math.max(deadline, now + minimumAlarmDelay));
694
- }));
747
+ if (Number.isFinite(deadline)) checkpoint.earlier(Math.max(deadline, now + minimumAlarmDelay));
748
+ });
695
749
  yield* failpoint.hit("maintenance:dirty:after");
696
750
  yield* Ref.update(activeMutations, (active) => active + 1);
697
751
  const enrolled = /* @__PURE__ */ new Set([...lanes, ...invalidatesRecovery ? [Native, ...publicationRequired ? [Publication] : []] : []]);
@@ -712,7 +766,7 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
712
766
  operation: "prearm maintenance mutation",
713
767
  message: "Run withMutation outside the source SQL transaction so prearming commits first"
714
768
  })) : Effect.acquireUseRelease(generationGate.withPermit(beginMutation(options?.invalidatesRecovery ?? true, options?.lanes ?? [])), (enrolled) => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(Effect.flatMap(CurrentMutationLanes, (current) => body.pipe(Effect.provideService(CurrentMutationLanes, [.../* @__PURE__ */ new Set([...current, ...enrolled])])))), Effect.tap(() => releaseLanes(enrolled, true).pipe(Effect.andThen(notify))), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), (enrolled) => endMutation(enrolled).pipe(Effect.andThen(notify))));
715
- const recordProgress = Effect.fn("ThreadMutationGate.recordProgress")(function* (lanes) {
769
+ const recordProgress = Effect.fnUntraced(function* (lanes) {
716
770
  yield* validateSourceBoundary;
717
771
  if (Option.isNone(yield* Effect.serviceOption(sql.transactionService))) return yield* DurableAlarmError.make({
718
772
  operation: "record source progress",
@@ -724,21 +778,21 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
724
778
  if (affected.size === 0) return;
725
779
  const now = yield* Clock.currentTimeMillis;
726
780
  const nativeSource = yield* CurrentNativeSource;
727
- yield* runTransaction("record maintenance producer progress", async () => {
781
+ yield* runAlarmTransaction("record maintenance producer progress", async (transaction, checkpoint) => {
728
782
  for (const id of affected) dueQueue.dirty(id, now, true);
729
783
  if (affected.has("effect-agent:native")) {
730
- const { state } = await readMaintenanceState(ctx.storage);
731
- await ctx.storage.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
784
+ const { state, initialized } = await readMaintenanceState(transaction);
785
+ if (!initialized || !nativeSource || state.retry !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
732
786
  ...Struct.omit(state, ["retry"]),
733
787
  dirty: state.dirty + (nativeSource ? 0n : 1n)
734
788
  })));
735
789
  }
736
- await ensureTransactionAlarmBy(ctx.storage, now + minimumAlarmDelay);
790
+ checkpoint.earlier(now + minimumAlarmDelay);
737
791
  });
738
792
  yield* notify;
739
793
  });
740
794
  return ThreadMutationGate.of({
741
- withTransaction: (body) => dueQueue.withTransaction(body).pipe(Effect.provideService(SqlClient, sql)),
795
+ withTransaction,
742
796
  withMutation,
743
797
  schedule,
744
798
  recordProgress,
@@ -759,7 +813,8 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
759
813
  * 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
760
814
  * producer keeps its generation dirty and immediately eligible. Quiescent native retries
761
815
  * retain their durable backoff.
762
- * 3. Admit at most two independent Thread Attempts, one active head per Thread.
816
+ * 3. Admit one Attempt for a single-Thread runtime, or at most two independent Thread
817
+ * Attempts for a shared host, with one active head per Thread.
763
818
  * Keep native and delivery admission open together while finite waves remain active, so
764
819
  * fresh replies and abort controls can progress during unrelated cleanup. Close atomically
765
820
  * at quiescence, or at the original ten-minute yield deadline, then join admitted waves.
@@ -775,7 +830,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
775
830
  const recoveryConfig = yield* DurableRuntimeConfig;
776
831
  const ledger = yield* SubmissionLedger;
777
832
  const wakes = yield* WakeScheduler;
778
- const alarm = yield* DurableAlarmService;
833
+ const hints = yield* DurableAlarmService;
834
+ const alarms = yield* ThreadMaintenanceAlarm;
779
835
  const config = yield* CloudflareDurableRuntimeConfig;
780
836
  const { ctx } = yield* DurableObjectContext;
781
837
  const dueQueue = make(ctx.storage);
@@ -789,12 +845,14 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
789
845
  const messages = yield* ThreadMessageDelivery;
790
846
  const host = yield* ThreadHostMaintenance;
791
847
  const framework = yield* ThreadNativeMaintenance;
848
+ const nativeDispatchConcurrency = () => framework.singleThreadRuntime === runtime ? 1 : 2;
792
849
  for (const lane of host.lanes) yield* Schema.decodeEffect(HostLaneId)(lane.id).pipe(Effect.mapError(alarmFailure("maintenance lane ID")));
793
850
  const recoveryEvents = Option.getOrElse(yield* Effect.serviceOption(ThreadRecoveryEvents), () => ThreadRecoveryEvents.of({ publish: () => Effect.void }));
794
851
  let recoveryEventsPending = false;
795
852
  const maintenancePassGate = yield* Semaphore.make(1);
796
853
  const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
797
854
  const runTransaction = yield* makeStorageOperation;
855
+ const { run: runAlarmTransaction } = yield* makeAlarmOperation;
798
856
  const queueSnapshot = runTransaction("read maintenance due queue", async () => dueQueue.read());
799
857
  const reportParked = Effect.gen(function* () {
800
858
  if (!(yield* queueSnapshot).some((row) => row.state === "parked" && row.reported === 0)) return;
@@ -834,13 +892,13 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
834
892
  let charged = false;
835
893
  return Effect.gen(function* () {
836
894
  const now = yield* Clock.currentTimeMillis;
837
- const claimed = yield* runTransaction("charge maintenance attempt", () => dueQueue.transaction(async (transaction) => {
895
+ const claimed = yield* runAlarmTransaction("charge maintenance attempt", async (_transaction, checkpoint) => {
838
896
  const claimed = dueQueue.claim(selected, now);
839
- const next$5 = next(dueQueue.read());
840
- if (Number.isFinite(next$5)) await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next$5));
841
- else await transaction.deleteAlarm();
897
+ const next$1 = next(dueQueue.read());
898
+ if (Number.isFinite(next$1)) checkpoint.at(Math.max(now + minimumAlarmDelay, next$1));
899
+ else checkpoint.cancel();
842
900
  return claimed;
843
- }));
901
+ });
844
902
  if (claimed === void 0) return Option.none();
845
903
  row = claimed;
846
904
  charged = true;
@@ -922,9 +980,11 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
922
980
  return recoveryEventsPending ? Option.some(0) : Option.none();
923
981
  })
924
982
  };
925
- const recordRecoveryFaults = Effect.fn("ThreadMaintenance.recordRecoveryFaults")(function* (result, recovery, current) {
983
+ const recordRecoveryFaults = Effect.fnUntraced(function* (result, recovery, current, selectedThread) {
926
984
  const threads = /* @__PURE__ */ new Map();
985
+ if (selectedThread !== void 0) threads.set(selectedThread, void 0);
927
986
  for (const report of result.reports) threads.set(report.threadId, void 0);
987
+ for (const report of result.workReports ?? []) threads.set(report.threadId, void 0);
928
988
  for (const blocked of result.blocked) threads.set(blocked.threadId, blocked.failure);
929
989
  if (threads.size === 0) return /* @__PURE__ */ new Map();
930
990
  const now = yield* Clock.currentTimeMillis;
@@ -985,14 +1045,35 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
985
1045
  for (const fault of retained.newlyBlocked) yield* Effect.logError("Native Thread recovery blocked; accepted work remains pending", fault.failure.reason === "defect" ? Cause.die(fault.failure) : Cause.fail(fault.failure)).pipe(Effect.annotateLogs({ threadId: fault.threadId }));
986
1046
  return retained.faults;
987
1047
  });
988
- const recoverThread = Effect.fn("ThreadMaintenance.recoverThread")(function* (threadId, recovery) {
989
- const result = yield* runtime.runRecovery({ threadId }).pipe(Effect.provideService(CurrentNativeSource, true));
990
- const faults = yield* recordRecoveryFaults(result, recovery);
991
- for (const report of result.reports) recovery.reports.set(report.submissionId, report);
992
- recovery.faults.delete(threadId);
993
- for (const [id, fault] of faults) recovery.faults.set(id, fault);
994
- recovery.recovered += result.reports.length + result.blocked.length;
995
- recovery.repaired ||= result.reports.some((report) => report.disposition === "repaired");
1048
+ const recoverThread = Effect.fnUntraced(function* (threadId, recovery) {
1049
+ let cursor = yield* runTransaction("read work recovery cursor", async () => {
1050
+ const retained = await ctx.storage.get(workCursorKey(threadId));
1051
+ if (retained === void 0) return void 0;
1052
+ const decoded = Schema.decodeUnknownSync(NativeWorkCursor)(retained);
1053
+ if (decoded.threadId !== threadId) throw new Error("Work cursor belongs to another Thread");
1054
+ return decoded.cursor;
1055
+ });
1056
+ do {
1057
+ const result = yield* runtime.runRecovery({
1058
+ threadId,
1059
+ ...cursor === void 0 ? {} : { cursor }
1060
+ }).pipe(Effect.provideService(CurrentNativeSource, true));
1061
+ const faults = yield* recordRecoveryFaults(result, recovery, void 0, threadId);
1062
+ yield* runTransaction("checkpoint work recovery cursor", async () => {
1063
+ if (result.cursor === void 0) await ctx.storage.delete(workCursorKey(threadId));
1064
+ else await ctx.storage.put(workCursorKey(threadId), Schema.encodeSync(NativeWorkCursor)({
1065
+ threadId,
1066
+ cursor: result.cursor
1067
+ }));
1068
+ });
1069
+ for (const report of result.reports) recovery.reports.set(report.submissionId, report);
1070
+ recovery.faults.delete(threadId);
1071
+ for (const [id, fault] of faults) recovery.faults.set(id, fault);
1072
+ recovery.recovered += (result.workReports?.length ?? result.reports.length) + result.blocked.length;
1073
+ recovery.repaired ||= (result.workReports ?? result.reports).some((report) => report.disposition === "repaired");
1074
+ for (const report of result.workReports ?? []) if (report.retryAtMillis !== void 0) recovery.retryAtMillis = Math.min(recovery.retryAtMillis ?? Infinity, report.retryAtMillis);
1075
+ cursor = result.blocked.length > 0 ? void 0 : result.cursor;
1076
+ } while (cursor !== void 0);
996
1077
  });
997
1078
  const readCurrentMaintenanceState = async (transaction) => {
998
1079
  const current = await readMaintenanceState(transaction);
@@ -1010,10 +1091,10 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1010
1091
  initialized: true
1011
1092
  };
1012
1093
  };
1013
- const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
1094
+ const ensureAlarm = Effect.fnUntraced(function* () {
1014
1095
  yield* failpoint.hit("maintenance:ensure:before");
1015
1096
  const now = yield* Clock.currentTimeMillis;
1016
- yield* runTransaction("ensure maintenance alarm", () => dueQueue.transaction(async (transaction) => {
1097
+ yield* runAlarmTransaction("ensure maintenance alarm", async (transaction, checkpoint) => {
1017
1098
  const { state, initialized } = await readCurrentMaintenanceState(transaction);
1018
1099
  recoveryEventsPending = hasRecoveryEvents(state);
1019
1100
  const rows = dueQueue.read();
@@ -1022,14 +1103,14 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1022
1103
  const native = state.dirty > state.processed ? state.retry?.generation === state.dirty ? state.retry.notBefore : rows.find((row) => row.id === "effect-agent:native")?.dueAt ?? now + minimumAlarmDelay : null;
1023
1104
  dueQueue.checkpointNative(native);
1024
1105
  const next$3 = next(dueQueue.read());
1025
- if (Number.isFinite(next$3)) await ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, next$3));
1026
- }));
1106
+ if (Number.isFinite(next$3)) checkpoint.earlier(Math.max(now + minimumAlarmDelay, next$3));
1107
+ });
1027
1108
  yield* failpoint.hit("maintenance:ensure:after");
1028
1109
  });
1029
- const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* (observed) {
1110
+ const beginPass = Effect.fnUntraced(function* (observed) {
1030
1111
  yield* failpoint.hit("maintenance:begin:before");
1031
1112
  const now = yield* Clock.currentTimeMillis;
1032
- const result = yield* runTransaction("begin maintenance pass", () => dueQueue.transaction(async (transaction) => {
1113
+ const result = yield* runAlarmTransaction("begin maintenance pass", async (transaction, checkpoint) => {
1033
1114
  const { state, initialized } = await readCurrentMaintenanceState(transaction);
1034
1115
  recoveryEventsPending = hasRecoveryEvents(state);
1035
1116
  observed.generation = state.dirty;
@@ -1050,8 +1131,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1050
1131
  observed.queue.set(Native, charged);
1051
1132
  observed.charged.set(Native, charged.revision);
1052
1133
  const deadline = next(dueQueue.read());
1053
- if (Number.isFinite(deadline)) await transaction.setAlarm(Math.max(now + minimumAlarmDelay, deadline));
1054
- else await transaction.deleteAlarm();
1134
+ if (Number.isFinite(deadline)) checkpoint.at(Math.max(now + minimumAlarmDelay, deadline));
1135
+ else checkpoint.cancel();
1055
1136
  return {
1056
1137
  _tag: "Actionable",
1057
1138
  generation: state.dirty,
@@ -1059,7 +1140,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1059
1140
  nonterminal: state.nonterminal,
1060
1141
  stalls: charged.stalls
1061
1142
  };
1062
- }));
1143
+ });
1063
1144
  yield* failpoint.hit("maintenance:begin:after");
1064
1145
  return result;
1065
1146
  });
@@ -1067,17 +1148,17 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1067
1148
  const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** Math.min(priorStalls, 30));
1068
1149
  return Math.ceil(backoff / 2 + backoff / 2 * jitter);
1069
1150
  };
1070
- const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed, priorStalls) {
1151
+ const rearmDelay = Effect.fnUntraced(function* (progressed, priorStalls) {
1071
1152
  return progressed ? config.alarmBackoffBase : backoffDelay(priorStalls, yield* Random.next);
1072
1153
  });
1073
- const rearmFailure = Effect.fn("ThreadMaintenance.rearmFailure")(function* (observed) {
1154
+ const rearmFailure = Effect.fnUntraced(function* (observed) {
1074
1155
  const { generation, nativeOnly } = observed;
1075
1156
  if (generation === void 0) return;
1076
1157
  yield* failpoint.hit("maintenance:retry:before");
1077
1158
  yield* mutations.withSnapshot((active) => Effect.gen(function* () {
1078
1159
  const now = yield* Clock.currentTimeMillis;
1079
1160
  const jitter = yield* Random.next;
1080
- yield* runTransaction("back off failed maintenance", () => dueQueue.transaction(async (transaction) => {
1161
+ yield* runAlarmTransaction("back off failed maintenance", async (transaction, checkpoint) => {
1081
1162
  const { state } = await readMaintenanceState(transaction);
1082
1163
  const previous = state.retry?.generation === generation && state.retry.nativeOnly === nativeOnly ? state.retry : void 0;
1083
1164
  const retry = state.retry?.generation === generation && state.retry.nativeOnly ? state.retry : MaintenanceRetry.make({
@@ -1096,27 +1177,25 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1096
1177
  const charged = observed.charged.get(row.id) === row.revision ? row : dueQueue.claim(row, now);
1097
1178
  if (charged !== void 0) dueQueue.complete(charged, now + backoffDelay(charged.stalls, jitter));
1098
1179
  }
1099
- const next$4 = next(dueQueue.read());
1100
- if (Number.isFinite(next$4)) await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next$4));
1101
- else await transaction.deleteAlarm();
1102
- }));
1180
+ const next$5 = next(dueQueue.read());
1181
+ if (Number.isFinite(next$5)) checkpoint.at(Math.max(now + minimumAlarmDelay, next$5));
1182
+ else checkpoint.cancel();
1183
+ });
1103
1184
  }));
1104
1185
  yield* failpoint.hit("maintenance:retry:after");
1105
1186
  });
1106
- const beginNative = Effect.fn("ThreadMaintenance.beginNative")(function* (observed) {
1107
- return yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
1108
- const generation = yield* beginPass(observed);
1109
- if (activeAtStart === 0) {
1110
- const native = (yield* queueSnapshot).find((row) => row.id === Native);
1111
- if (native !== void 0) observed.available.set(native.id, native.revision);
1112
- }
1113
- return {
1114
- ...generation,
1115
- activeAtStart
1116
- };
1117
- }));
1118
- });
1119
- const attemptSelected = Effect.fn("ThreadMaintenance.attemptSelected")(function* (selected, previousWaits, yieldAfter, recovery) {
1187
+ const beginNative = (observed) => mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
1188
+ const generation = yield* beginPass(observed);
1189
+ if (activeAtStart === 0) {
1190
+ const native = (yield* queueSnapshot).find((row) => row.id === Native);
1191
+ if (native !== void 0) observed.available.set(native.id, native.revision);
1192
+ }
1193
+ return {
1194
+ ...generation,
1195
+ activeAtStart
1196
+ };
1197
+ }));
1198
+ const attemptSelected = Effect.fnUntraced(function* (selected, previousWaits, yieldAfter, recovery) {
1120
1199
  yield* recoverThread(selected.threadId, recovery);
1121
1200
  let bindingFailure;
1122
1201
  const settlement = recovery.faults.has(selected.threadId) ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.provideService(CurrentNativeSource, true), Effect.catchTag("BindingUnavailable", (failure) => {
@@ -1148,7 +1227,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1148
1227
  }
1149
1228
  return Option.isSome(settlement) ? 1 : 0;
1150
1229
  });
1151
- const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed, recovery, native, dispatch = true, reserved = new Set(native.active.keys())) {
1230
+ const advance = Effect.fnUntraced(function* (started, yieldAfter, observed, recovery, native, dispatch = true, reserved = new Set(native.active.keys())) {
1152
1231
  let publicationRow = (yield* queueSnapshot).find((row) => row.id === Publication);
1153
1232
  if (publicationRow !== void 0 && publicationRow.dueAt !== null && publicationRow.dueAt <= (yield* Clock.currentTimeMillis)) {
1154
1233
  yield* runQueued(publicationRow, mutations.withSnapshot((active) => started._tag === "Actionable" && active === 0 ? publication.prepareGeneration(started.generation) : Effect.void).pipe(Effect.andThen(publication.drain)), observed);
@@ -1186,7 +1265,20 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1186
1265
  generation: started.generation,
1187
1266
  activeAtStart: started.activeAtStart
1188
1267
  };
1268
+ const { scannedGeneration, afterWorkThread } = yield* runTransaction("read work discovery position", async () => {
1269
+ const { state } = await readMaintenanceState(ctx.storage);
1270
+ return {
1271
+ scannedGeneration: state.dirty,
1272
+ afterWorkThread: state.lastRecoveredThreadId
1273
+ };
1274
+ });
1189
1275
  const current = yield* Stream.runCollect(ledger.scanNonterminal);
1276
+ let workOwners = yield* runtime.discoverWorkThreads({
1277
+ limit: 32,
1278
+ ...afterWorkThread === void 0 ? {} : { afterThreadId: afterWorkThread }
1279
+ }).pipe(Effect.provideService(CurrentNativeSource, true));
1280
+ if (workOwners.threadIds.length === 0 && afterWorkThread !== void 0) workOwners = yield* runtime.discoverWorkThreads({ limit: 32 }).pipe(Effect.provideService(CurrentNativeSource, true));
1281
+ recovery.discoveryPending ||= workOwners.afterThreadId !== void 0;
1190
1282
  const selectionTime = yield* Clock.currentTimeMillis;
1191
1283
  const submissionsByThread = /* @__PURE__ */ new Map();
1192
1284
  for (const row of current) {
@@ -1194,6 +1286,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1194
1286
  ids.push(row.submissionId);
1195
1287
  submissionsByThread.set(row.threadId, ids);
1196
1288
  }
1289
+ for (const threadId of workOwners.threadIds) if (!submissionsByThread.has(threadId)) submissionsByThread.set(threadId, []);
1197
1290
  yield* runTransaction("read Thread recovery deadlines", async () => {
1198
1291
  for (const [threadId, ids] of submissionsByThread) {
1199
1292
  if (!recovery.loaded.has(threadId)) {
@@ -1265,13 +1358,13 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1265
1358
  const waits = (state.bindingWaits ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
1266
1359
  const runnable = eligible.filter((threadId) => !reserved.has(threadId) && !native.active.has(threadId) && !native.deferred.has(threadId) && !waits.some((wait) => wait.threadId === threadId));
1267
1360
  const pivot = Math.max(0, runnable.findIndex((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId));
1268
- const selected = dispatch ? [...runnable.slice(pivot), ...runnable.slice(0, pivot)].slice(0, nativeDispatchConcurrency - reserved.size) : [];
1361
+ const selected = dispatch ? [...runnable.slice(pivot), ...runnable.slice(0, pivot)].slice(0, nativeDispatchConcurrency() - reserved.size) : [];
1269
1362
  const next = selected.at(-1);
1270
1363
  if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
1271
1364
  ...state,
1272
1365
  lastServedThreadId: next
1273
1366
  })));
1274
- const backlog = recovery.started ? [] : [...heads.keys()].filter((threadId) => {
1367
+ const backlog = recovery.started ? [] : [.../* @__PURE__ */ new Set([...heads.keys(), ...workOwners.threadIds])].filter((threadId) => {
1275
1368
  const fault = recoveryFaults.get(threadId);
1276
1369
  return !eligible.includes(threadId) && (fault === void 0 || fault.retryAt <= selectionTime);
1277
1370
  });
@@ -1280,13 +1373,15 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1280
1373
  return {
1281
1374
  selected,
1282
1375
  waits,
1283
- backlog: [...after, ...before]
1376
+ backlog: [...after, ...before].slice(0, 32),
1377
+ discoveryPending: backlog.length > 32
1284
1378
  };
1285
1379
  }));
1286
1380
  yield* failpoint.hit("maintenance:select:after");
1287
1381
  if (!recovery.started) {
1288
1382
  recovery.started = true;
1289
1383
  recovery.needsCheckpoint = selection.backlog.length > 0;
1384
+ recovery.discoveryPending ||= selection.discoveryPending;
1290
1385
  for (const threadId of selection.backlog) recovery.pending.add(threadId);
1291
1386
  yield* Deferred.succeed(recovery.queue, selection.backlog);
1292
1387
  }
@@ -1302,10 +1397,9 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1302
1397
  nextAttemptAt: void 0,
1303
1398
  dispatched: selection.selected.length > 0
1304
1399
  };
1305
- const scannedGeneration = yield* runTransaction("observe native source generation", async () => (await readMaintenanceState(ctx.storage)).state.dirty);
1306
1400
  const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
1307
1401
  const waitingHeads = /* @__PURE__ */ new Map();
1308
- const autonomous = remaining.some((snapshot) => {
1402
+ const autonomous = recovery.pending.size > 0 || recovery.discoveryPending || recoveryFaults.size > 0 || recovery.retryAtMillis !== void 0 || remaining.some((snapshot) => {
1309
1403
  if (snapshot.state === "unknown" && waiting(snapshot)) return false;
1310
1404
  const headWaiting = waitingHeads.get(snapshot.threadId);
1311
1405
  if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, waiting(snapshot));
@@ -1316,13 +1410,14 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1316
1410
  const now = yield* Clock.currentTimeMillis;
1317
1411
  const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
1318
1412
  const nextEligible = [...recoveryFaults.values()].map((fault) => fault.retryAt).concat(eligible.filter((threadId) => !selection.waits.some((wait) => wait.threadId === threadId)).map(() => now));
1413
+ if (recovery.retryAtMillis !== void 0) nextEligible.push(recovery.retryAtMillis);
1319
1414
  const retryDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
1320
1415
  const delay = Math.max(ordinaryDelay, retryDelay);
1321
1416
  yield* failpoint.hit("maintenance:checkpoint:before");
1322
1417
  const nextAttemptAt = yield* mutations.withSnapshot((active) => runTransaction("checkpoint native maintenance", () => dueQueue.transaction(async (transaction) => {
1323
1418
  const { state } = await readMaintenanceState(transaction);
1324
1419
  const mutationOverlap = observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0;
1325
- const generation = remaining.length === 0 && !mutationOverlap && state.dirty === scannedGeneration ? scannedGeneration : observation.generation;
1420
+ const generation = remaining.length === 0 && workOwners.threadIds.length === 0 && !mutationOverlap && state.dirty === scannedGeneration ? scannedGeneration : observation.generation;
1326
1421
  const processed = autonomous || mutationOverlap ? state.processed : state.processed > generation ? state.processed : generation;
1327
1422
  const canBackoff = !mutationOverlap && state.dirty === started.generation;
1328
1423
  const next = ThreadMaintenanceState.make({
@@ -1343,7 +1438,10 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1343
1438
  return dueAt ?? void 0;
1344
1439
  })));
1345
1440
  yield* failpoint.hit("maintenance:checkpoint:after");
1346
- if (remaining.length === 0 && recovery.pending.size === 0) delete recovery.observation;
1441
+ if (remaining.length === 0 && recovery.pending.size === 0) recovery.observation = {
1442
+ generation: observation.generation,
1443
+ activeAtStart: 0
1444
+ };
1347
1445
  native.deferred.clear();
1348
1446
  native.progressed = false;
1349
1447
  native.needsCheckpoint = false;
@@ -1353,7 +1451,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1353
1451
  nextAttemptAt
1354
1452
  };
1355
1453
  });
1356
- const validateLanes = Effect.fn("ThreadMaintenance.validateLanes")(function* (lanes, queued) {
1454
+ const validateLanes = Effect.fnUntraced(function* (lanes, queued) {
1357
1455
  const ids = /* @__PURE__ */ new Set([
1358
1456
  Native,
1359
1457
  Publication,
@@ -1373,7 +1471,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1373
1471
  message: `No handler registered for scheduled lane: ${row.id}`
1374
1472
  });
1375
1473
  });
1376
- const dispatch = Effect.fn("ThreadMaintenance.dispatch")(function* (yieldAfter, dispatchUntil, observed) {
1474
+ const dispatch = Effect.fnUntraced(function* (yieldAfter, dispatchUntil, observed) {
1377
1475
  const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.asVoid, Effect.catch(() => Effect.never));
1378
1476
  const started = yield* beginNative(observed);
1379
1477
  yield* mutations.withSnapshot(() => Effect.gen(function* () {
@@ -1408,7 +1506,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1408
1506
  started: false,
1409
1507
  needsCheckpoint: false,
1410
1508
  recovered: 0,
1411
- repaired: false
1509
+ repaired: false,
1510
+ discoveryPending: false
1412
1511
  };
1413
1512
  const recoveryFiber = yield* fork(Effect.gen(function* () {
1414
1513
  for (const threadId of yield* Deferred.await(recovery.queue)) {
@@ -1459,7 +1558,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1459
1558
  native = void 0;
1460
1559
  if (result.phase === "actionable") phase = "actionable";
1461
1560
  observed.nativeOnly = dispatch.active.size > 0;
1462
- if (result.dispatched === true && dispatch.active.size < nativeDispatchConcurrency) {
1561
+ if (result.dispatched === true && dispatch.active.size < nativeDispatchConcurrency()) {
1463
1562
  dispatch.scanGeneration = void 0;
1464
1563
  nativeCheck = true;
1465
1564
  }
@@ -1548,7 +1647,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1548
1647
  }), observed).pipe(Effect.asVoid));
1549
1648
  }
1550
1649
  if (native === void 0 && dispatch.active.size === 0 && recoveryObserved && !recovery.needsCheckpoint && backfillObserved && delivery === void 0 && lanes.every((lane) => lane.fiber === void 0)) break;
1551
- if (native === void 0 && nativeCheck && dispatch.active.size < nativeDispatchConcurrency) {
1650
+ if (native === void 0 && nativeCheck && dispatch.active.size < nativeDispatchConcurrency()) {
1552
1651
  nativeCheck = false;
1553
1652
  const nativeCheckpoint = dispatch.needsCheckpoint && dispatch.active.size === 0;
1554
1653
  const checkpoint = recoveryObserved && recovery.needsCheckpoint || nativeCheckpoint;
@@ -1610,7 +1709,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1610
1709
  settled: dispatch.settled
1611
1710
  };
1612
1711
  });
1613
- const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
1712
+ const pass = Effect.fnUntraced(function* (yieldAfter, dispatchUntil, observed) {
1614
1713
  const concurrent = yield* Effect.exit(Effect.scoped(dispatch(yieldAfter, dispatchUntil, observed)));
1615
1714
  const afterNative = yield* Effect.gen(function* () {
1616
1715
  const lanes = [
@@ -1678,18 +1777,18 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1678
1777
  yield* failpoint.hit("maintenance:finish:before");
1679
1778
  const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
1680
1779
  const now = yield* Clock.currentTimeMillis;
1681
- return yield* runTransaction("finish maintenance event", () => dueQueue.transaction(async (transaction) => {
1780
+ return yield* runAlarmTransaction("finish maintenance event", async (transaction, checkpoint) => {
1682
1781
  const { state } = await readMaintenanceState(transaction);
1683
1782
  const native = active > 0 || state.dirty > state.processed ? state.retry?.generation === state.dirty && active === 0 ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : state.dirty === observed.generation && active === 0 ? result.nextAttemptAt ?? now + minimumAlarmDelay : now + minimumAlarmDelay : Infinity;
1684
1783
  dueQueue.checkpointNative(Number.isFinite(native) ? native : null);
1685
- const next$6 = next(dueQueue.read());
1686
- if (Number.isFinite(next$6)) {
1687
- await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next$6));
1784
+ const next$2 = next(dueQueue.read());
1785
+ if (Number.isFinite(next$2)) {
1786
+ checkpoint.at(Math.max(now + minimumAlarmDelay, next$2));
1688
1787
  return "rearmed";
1689
1788
  }
1690
- await transaction.deleteAlarm();
1789
+ checkpoint.cancel();
1691
1790
  return "cleared";
1692
- }));
1791
+ });
1693
1792
  }));
1694
1793
  yield* failpoint.hit("maintenance:finish:after");
1695
1794
  const report = MaintenancePassReport.make({
@@ -1719,7 +1818,10 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1719
1818
  charged: /* @__PURE__ */ new Map(),
1720
1819
  available: /* @__PURE__ */ new Map()
1721
1820
  };
1722
- return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(dueQueue.withView(Effect.scoped(pass(yieldAfter, dispatchUntil, observed))).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed)))));
1821
+ return yield* alarms.deferWakes(maintenancePassGate.withPermit(dueQueue.withView(Effect.scoped(pass(yieldAfter, dispatchUntil, observed))).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed))))).pipe(hints.withHintsDeferred, Effect.catchTags({
1822
+ StorageOperationError: (error) => alarmFailure("defer maintenance wakes")(error),
1823
+ InvalidScheduleConfigurationError: (error) => alarmFailure("defer maintenance wakes")(error)
1824
+ }));
1723
1825
  }).pipe(Effect.timeoutOrElse({
1724
1826
  duration: "14 minutes",
1725
1827
  orElse: () => DurableAlarmError.make({
@@ -1733,6 +1835,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
1733
1835
  }));
1734
1836
  };
1735
1837
  //#endregion
1736
- export { Messages as _, ThreadHostMaintenance as a, Publication as b, ThreadMessageDelivery as c, ThreadPublication as d, ThreadRecoveryEvents as f, Lifecycle as g, HostLaneId as h, MaintenancePassReport as i, ThreadMutationGate as l, publishCommitted as m, DurableAlarmError as n, ThreadMaintenance as o, ThreadRecoveryFaultEvent as p, DurableAlarmService as r, ThreadMaintenanceFailpoint as s, Alarm_exports as t, ThreadNativeMaintenance as u, Native as v, Projection as y };
1838
+ export { Lifecycle as _, ThreadHostMaintenance as a, Projection as b, ThreadMaintenanceFailpoint as c, ThreadNativeMaintenance as d, ThreadPublication as f, HostLaneId as g, publishCommitted as h, MaintenancePassReport as i, ThreadMessageDelivery as l, ThreadRecoveryFaultEvent as m, DurableAlarmError as n, ThreadMaintenance as o, ThreadRecoveryEvents as p, DurableAlarmService as r, ThreadMaintenanceAlarm as s, Alarm_exports as t, ThreadMutationGate as u, Messages as v, Publication as x, Native as y };
1737
1839
 
1738
- //# sourceMappingURL=Alarm-B0ODr1NE.mjs.map
1840
+ //# sourceMappingURL=Alarm-BMEAZo3p.mjs.map