@effect-agent/platform-cloudflare 0.1.0-beta.111 → 0.1.0-beta.112

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/Alarm.d.mts CHANGED
@@ -2,13 +2,15 @@ import { DurableObjectContext } from "./CloudflareBindings.mjs";
2
2
  import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-DKXGo8L3.mjs";
3
3
  import { Cause, Context, DateTime, Effect, Layer, Option, Schema, Scope } from "effect";
4
4
  import { DurableBindingFailure } from "effect-agent/agent-registration";
5
- import { DurableAgentRuntime, DurableWorkerFailure } from "effect-agent/durable-agent-runtime";
5
+ import { DurableAgentRuntime, DurableRuntimeConfig, DurableWorkerFailure, RecoveryFailure } from "effect-agent/durable-agent-runtime";
6
+ import { ThreadId } from "effect-agent/identifiers";
7
+ import { OperationDenied } from "effect-agent/operation-authorizer";
6
8
  import { SubmissionLedger } from "effect-agent/submission-ledger";
7
9
  import { ThreadProjectionError, ThreadProjectionMaintenance } from "effect-agent/thread-projection-maintenance";
8
10
  import { WakeScheduler } from "effect-agent/wake-scheduler";
9
11
  import { SqlClient } from "effect/unstable/sql/SqlClient";
10
12
  declare namespace Alarm_d_exports {
11
- export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, publishCommitted };
13
+ export { DurableAlarmError, DurableAlarmService, MaintenancePassFailure, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMaintenanceFailpointHandler, ThreadMaintenanceFailpointLocation, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadPublicationService, ThreadRecoveryFault, publishCommitted };
12
14
  }
13
15
  declare const DurableAlarmError_base: Schema.Class<DurableAlarmError, Schema.TaggedStruct<"DurableAlarmError", {
14
16
  readonly operation: Schema.String;
@@ -64,7 +66,7 @@ export declare class DurableAlarmService extends DurableAlarmService_base {
64
66
  declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Schema.Struct<{
65
67
  /** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
66
68
  readonly phase: Schema.Literals<readonly ["caught-up", "actionable"]>;
67
- /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
69
+ /** Recovery decisions and Thread faults observed during this event. */
68
70
  readonly recovered: Schema.Int;
69
71
  /** Head Attempts settled during the event. Joined input may settle with each head. */
70
72
  readonly settled: Schema.Int;
@@ -76,7 +78,7 @@ declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Sc
76
78
  /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
77
79
  export declare class MaintenancePassReport extends MaintenancePassReport_base {}
78
80
  /** Fault boundaries around every maintenance-owned durable mutation. */
79
- export type ThreadMaintenanceFailpointLocation = "maintenance:dirty:before" | "maintenance:dirty:after" | "maintenance:mutation:armed" | "maintenance:mutation:finished" | "maintenance:ensure:before" | "maintenance:ensure:after" | "maintenance:begin:before" | "maintenance:begin:after" | "maintenance:select:before" | "maintenance:select:after" | "maintenance:binding-retry:before" | "maintenance:binding-retry:after" | "maintenance:retry:before" | "maintenance:retry:after" | "maintenance:checkpoint:before" | "maintenance:checkpoint:after" | "maintenance:finish:before" | "maintenance:finish:after";
81
+ export type ThreadMaintenanceFailpointLocation = "maintenance:dirty:before" | "maintenance:dirty:after" | "maintenance:mutation:armed" | "maintenance:mutation:finished" | "maintenance:ensure:before" | "maintenance:ensure:after" | "maintenance:begin:before" | "maintenance:begin:after" | "maintenance:select:before" | "maintenance:select:after" | "maintenance:binding-retry:before" | "maintenance:binding-retry:after" | "maintenance:recovery-status:before" | "maintenance:recovery-status:after" | "maintenance:retry:before" | "maintenance:retry:after" | "maintenance:checkpoint:before" | "maintenance:checkpoint:after" | "maintenance:finish:before" | "maintenance:finish:after";
80
82
  export type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
81
83
  declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
82
84
  readonly hit: ThreadMaintenanceFailpointHandler;
@@ -146,6 +148,23 @@ export declare const ThreadHostMaintenance: Context.Reference<{
146
148
  }>;
147
149
  /** @internal A committed source operation must not become a failed operation because delivery failed. */
148
150
  export declare const publishCommitted: Effect.Effect<void, never, ThreadPublication>;
151
+ declare const ThreadRecoveryFault_base: Schema.Class<ThreadRecoveryFault, Schema.Struct<{
152
+ readonly schemaVersion: Schema.Literal<1>;
153
+ readonly threadId: Schema.brand<Schema.NonEmptyString, "@effect-agent/core/ThreadId">;
154
+ readonly firstFailedAt: Schema.Finite;
155
+ readonly lastFailedAt: Schema.Finite;
156
+ /** Earliest automatic recovery retry; new admissions do not erase this deadline. */
157
+ readonly retryAt: Schema.Finite;
158
+ /** Saturates at 2^31 - 1; one observation per Thread per recovery sweep. */
159
+ readonly attempts: Schema.Int;
160
+ readonly failure: typeof RecoveryFailure;
161
+ }>, {}>;
162
+ /**
163
+ * A durable observation of blocked recovery, independent of the execution journal. This is
164
+ * neither a Settlement nor proof that external effects did not happen. A successful recovery
165
+ * sweep clears it; repair must preserve canonical history and the original admission identity.
166
+ */
167
+ export declare class ThreadRecoveryFault extends ThreadRecoveryFault_base {}
149
168
  declare const ThreadMutationGate_base: Context.ServiceClass<ThreadMutationGate, "@effect-agent/platform-cloudflare/internal/ThreadMutationGate", {
150
169
  readonly withMutation: <A, E, R>(body: Effect.Effect<A, E, R>,
151
170
  /**
@@ -175,6 +194,13 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
175
194
  * generation has an alarm. It never scans the ledger or canonical history.
176
195
  */
177
196
  readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
197
+ /**
198
+ * Authorize `explain`, then read one bounded local record without reading execution history.
199
+ * None means no recorded fault, not proof of health or settlement. The host authenticates
200
+ * callers and verifies local Thread membership before exposing this service across RPC.
201
+ * Provide OperationAuthorizer when constructing this Layer, as for DurableAgentRuntime.
202
+ */
203
+ readonly recoveryStatus: (threadId: ThreadId) => Effect.Effect<Option.Option<ThreadRecoveryFault>, DurableAlarmError | OperationDenied>;
178
204
  /**
179
205
  * Serialize the pre-arm boundary with pass acknowledgement, advance the durable dirty
180
206
  * generation and arm the alarm in one transaction BEFORE running the caller's mutation.
@@ -191,9 +217,9 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
191
217
  * recovery, ledger scans or canonical-history reads.
192
218
  * 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
193
219
  * producer keeps its newer generation dirty. Native retries retain their durable backoff.
194
- * 3. After the initial native opportunity, stop admitting new external waves and let the
195
- * active waves finish. While they remain in flight, native wakes and bounded scans can
196
- * advance more heads. All Attempts share the event's original ten-minute yield deadline.
220
+ * 3. Keep delivery admission open while old recovery is pending, so fresh native replies can
221
+ * publish in this event. Close once, at latest at the original ten-minute yield deadline;
222
+ * native wakes can still advance heads while admitted deliveries retire.
197
223
  * 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill joins are
198
224
  * bounded independently; incoming native work never restarts or cancels their attempts.
199
225
  * Auxiliary failures are reported after the current native opportunity.
@@ -201,7 +227,7 @@ declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@
201
227
  * The whole event retains one fourteen-minute cooperative timeout.
202
228
  */
203
229
  export declare class ThreadMaintenance extends ThreadMaintenance_base {
204
- static readonly layer: Layer.Layer<ThreadMaintenance, never, ThreadMutationGate | ThreadPublication | ThreadProjectionMaintenance | DurableAgentRuntime | SubmissionLedger | WakeScheduler | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient>;
230
+ static readonly layer: Layer.Layer<ThreadMaintenance, never, ThreadMutationGate | ThreadPublication | ThreadProjectionMaintenance | DurableAgentRuntime | DurableRuntimeConfig | SubmissionLedger | WakeScheduler | DurableAlarmService | ThreadMaintenanceFailpoint | CloudflareDurableRuntimeConfig | DurableObjectContext | SqlClient>;
205
231
  }
206
232
  //#endregion
207
233
  export { Alarm_d_exports as t };
package/dist/Alarm.mjs CHANGED
@@ -2,11 +2,12 @@ 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 { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Schema, Scope, Semaphore, Stream, Struct } from "effect";
5
+ import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Result, Schema, Scope, Semaphore, Stream, Struct } from "effect";
6
6
  import "effect-agent/agent-registration";
7
- import { DurableAgentRuntime } from "effect-agent/durable-agent-runtime";
7
+ import { DurableAgentRuntime, DurableRuntimeConfig, RecoveryBlocked, RecoveryFailure, RecoverySweepResult, isolateRecovery } from "effect-agent/durable-agent-runtime";
8
8
  import { SubmissionId, ThreadId } from "effect-agent/identifiers";
9
- import { SubmissionLedger } from "effect-agent/submission-ledger";
9
+ import { OperationAuthorizationRequest, OperationAuthorizer } from "effect-agent/operation-authorizer";
10
+ import { AbortIntentRequest, SubmissionLedger } from "effect-agent/submission-ledger";
10
11
  import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
11
12
  import { WakeScheduler } from "effect-agent/wake-scheduler";
12
13
  import { SqlClient } from "effect/unstable/sql/SqlClient";
@@ -21,6 +22,7 @@ var Alarm_exports = /* @__PURE__ */ __exportAll({
21
22
  ThreadMessageDelivery: () => ThreadMessageDelivery,
22
23
  ThreadMutationGate: () => ThreadMutationGate,
23
24
  ThreadPublication: () => ThreadPublication,
25
+ ThreadRecoveryFault: () => ThreadRecoveryFault,
24
26
  publishCommitted: () => publishCommitted
25
27
  });
26
28
  /**
@@ -87,7 +89,7 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
87
89
  var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
88
90
  /** `caught-up` ran no runtime work (publication may be pending); `actionable` ran recovery. */
89
91
  phase: Schema.Literals(["caught-up", "actionable"]),
90
- /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
92
+ /** Recovery decisions and Thread faults observed during this event. */
91
93
  recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
92
94
  /** Head Attempts settled during the event. Joined input may settle with each head. */
93
95
  settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
@@ -155,6 +157,30 @@ var BindingRetry = class extends Schema.Class("BindingRetry")({
155
157
  notBefore: Schema.Finite,
156
158
  reportedAt: Schema.Finite
157
159
  }) {};
160
+ /**
161
+ * A durable observation of blocked recovery, independent of the execution journal. This is
162
+ * neither a Settlement nor proof that external effects did not happen. A successful recovery
163
+ * sweep clears it; repair must preserve canonical history and the original admission identity.
164
+ */
165
+ var ThreadRecoveryFault = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadRecoveryFault")({
166
+ schemaVersion: Schema.Literal(1),
167
+ threadId: ThreadId,
168
+ firstFailedAt: Schema.Finite,
169
+ lastFailedAt: Schema.Finite,
170
+ /** Earliest automatic recovery retry; new admissions do not erase this deadline. */
171
+ retryAt: Schema.Finite,
172
+ /** Saturates at 2^31 - 1; one observation per Thread per recovery sweep. */
173
+ attempts: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2147483647)),
174
+ failure: RecoveryFailure
175
+ }) {};
176
+ const recoveryFaultKey = (threadId) => `effect-agent:thread-recovery-fault:v1:${threadId}`;
177
+ const decodeRecoveryFaultValue = Schema.decodeUnknownSync(ThreadRecoveryFault);
178
+ const decodeRecoveryFault = (threadId, encoded) => {
179
+ const fault = decodeRecoveryFaultValue(encoded);
180
+ if (fault.threadId !== threadId) throw new Error("Recovery status does not match its Thread key");
181
+ return fault;
182
+ };
183
+ const encodeRecoveryFault = Schema.encodeSync(ThreadRecoveryFault);
158
184
  var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
159
185
  generation: MaintenanceGeneration,
160
186
  notBefore: Schema.Finite,
@@ -169,6 +195,8 @@ var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-
169
195
  nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
170
196
  /** One physical-owner cursor; old single-lane records need no conversion. */
171
197
  lastServedThreadId: Schema.optionalKey(ThreadId),
198
+ /** Rotate old recovery independently of dispatch, including after eviction or timeout. */
199
+ lastRecoveredThreadId: Schema.optionalKey(ThreadId),
172
200
  bindingRetries: Schema.optionalKey(Schema.Array(BindingRetry)),
173
201
  /** Absent on older records. A newer mutation makes this retry obsolete. */
174
202
  retry: Schema.optionalKey(MaintenanceRetry)
@@ -258,9 +286,9 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
258
286
  * recovery, ledger scans or canonical-history reads.
259
287
  * 2. Reconcile before each head Attempt, then checkpoint only the observed generation. A racing
260
288
  * producer keeps its newer generation dirty. Native retries retain their durable backoff.
261
- * 3. After the initial native opportunity, stop admitting new external waves and let the
262
- * active waves finish. While they remain in flight, native wakes and bounded scans can
263
- * advance more heads. All Attempts share the event's original ten-minute yield deadline.
289
+ * 3. Keep delivery admission open while old recovery is pending, so fresh native replies can
290
+ * publish in this event. Close once, at latest at the original ten-minute yield deadline;
291
+ * native wakes can still advance heads while admitted deliveries retire.
264
292
  * 4. Native message delivery retains its driver-owned Claim deadline. Host/backfill joins are
265
293
  * bounded independently; incoming native work never restarts or cancels their attempts.
266
294
  * Auxiliary failures are reported after the current native opportunity.
@@ -270,6 +298,7 @@ var ThreadMutationGate = class ThreadMutationGate extends Context.Service()("@ef
270
298
  var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
271
299
  static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
272
300
  const runtime = yield* DurableAgentRuntime;
301
+ const recoveryConfig = yield* DurableRuntimeConfig;
273
302
  const ledger = yield* SubmissionLedger;
274
303
  const wakes = yield* WakeScheduler;
275
304
  const alarm = yield* DurableAlarmService;
@@ -281,6 +310,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
281
310
  const projection = yield* ThreadProjectionMaintenance;
282
311
  const messages = yield* ThreadMessageDelivery;
283
312
  const host = yield* ThreadHostMaintenance;
313
+ const authorizer = yield* OperationAuthorizer;
284
314
  const projectionDeadline = projection.pendingDeadline.pipe(Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError("Thread projection deadline unavailable", cause).pipe(Effect.as(Option.some(0)))));
285
315
  const pendingDeadline = Effect.gen(function* () {
286
316
  return earliestDeadline(earliestDeadline(yield* publication.pendingDeadline, yield* messages.pendingDeadline), earliestDeadline(yield* projectionDeadline, yield* host.pendingDeadline));
@@ -288,6 +318,65 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
288
318
  const maintenancePassGate = yield* Semaphore.make(1);
289
319
  const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
290
320
  const runTransaction = yield* makeStorageOperation;
321
+ const recoveryStatus = Effect.fn("ThreadMaintenance.recoveryStatus")(function* (threadId) {
322
+ yield* authorizer.authorize(OperationAuthorizationRequest.make({
323
+ operation: "explain",
324
+ threadId
325
+ }));
326
+ return yield* runTransaction("read Thread recovery status", async () => {
327
+ const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
328
+ return encoded === void 0 ? Option.none() : Option.some(decodeRecoveryFault(threadId, encoded));
329
+ });
330
+ });
331
+ const recordRecoveryStatus = Effect.fn("ThreadMaintenance.recordRecoveryStatus")(function* (result) {
332
+ const threads = /* @__PURE__ */ new Map();
333
+ for (const report of result.reports) threads.set(report.threadId, void 0);
334
+ for (const blocked of result.blocked) threads.set(blocked.threadId, blocked.failure);
335
+ if (threads.size === 0) return /* @__PURE__ */ new Map();
336
+ const now = yield* Clock.currentTimeMillis;
337
+ yield* failpoint.hit("maintenance:recovery-status:before");
338
+ const retained = yield* runTransaction("record Thread recovery status", () => ctx.storage.transaction(async (transaction) => {
339
+ const newlyBlocked = [];
340
+ const faults = /* @__PURE__ */ new Map();
341
+ for (const [threadId, failure] of threads) {
342
+ const key = recoveryFaultKey(threadId);
343
+ const encoded = await transaction.get(key);
344
+ const previous = encoded === void 0 ? void 0 : decodeRecoveryFault(threadId, encoded);
345
+ if (failure === void 0) {
346
+ if (previous !== void 0) await transaction.delete(key);
347
+ continue;
348
+ }
349
+ const fault = ThreadRecoveryFault.make({
350
+ schemaVersion: 1,
351
+ threadId,
352
+ firstFailedAt: previous?.firstFailedAt ?? now,
353
+ lastFailedAt: now,
354
+ attempts: Math.min(2147483647, (previous?.attempts ?? 0) + 1),
355
+ retryAt: now + Math.min(6e4, 5e3 * 2 ** Math.min(30, previous?.attempts ?? 0)),
356
+ failure
357
+ });
358
+ await transaction.put(key, encodeRecoveryFault(fault));
359
+ faults.set(threadId, fault);
360
+ if (previous === void 0) newlyBlocked.push(fault);
361
+ }
362
+ return {
363
+ newlyBlocked,
364
+ faults
365
+ };
366
+ }));
367
+ yield* failpoint.hit("maintenance:recovery-status:after");
368
+ 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 }));
369
+ return retained.faults;
370
+ });
371
+ const recoverThread = Effect.fn("ThreadMaintenance.recoverThread")(function* (threadId, recovery) {
372
+ const result = yield* runtime.runRecovery({ threadId });
373
+ const faults = yield* recordRecoveryStatus(result);
374
+ for (const report of result.reports) recovery.reports.set(report.submissionId, report);
375
+ recovery.faults.delete(threadId);
376
+ for (const [id, fault] of faults) recovery.faults.set(id, fault);
377
+ recovery.recovered += result.reports.length + result.blocked.length;
378
+ recovery.repaired ||= result.reports.some((report) => report.disposition === "repaired");
379
+ });
291
380
  const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
292
381
  yield* failpoint.hit("maintenance:ensure:before");
293
382
  const now = yield* Clock.currentTimeMillis;
@@ -368,7 +457,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
368
457
  };
369
458
  }));
370
459
  });
371
- const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed) {
460
+ const advance = Effect.fn("ThreadMaintenance.advance")(function* (started, yieldAfter, observed, recovery, dispatch = true) {
372
461
  const deadline = yield* publication.pendingDeadline;
373
462
  if (started._tag === "Actionable" || Option.isSome(deadline) && deadline.value <= (yield* Clock.currentTimeMillis)) yield* publication.drain;
374
463
  const pending = yield* publication.pendingDeadline;
@@ -382,43 +471,91 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
382
471
  }));
383
472
  return {
384
473
  phase: "caught-up",
385
- recovered: 0,
386
474
  settled: 0,
387
475
  nonterminal: started.nonterminal,
388
476
  nextAttemptAt
389
477
  };
390
478
  }
391
479
  observed.nativeOnly = true;
392
- const recovered = yield* runtime.runRecovery;
393
- const reports = new Map(recovered.map((report) => [report.submissionId, report]));
480
+ const observation = recovery.observation ??= {
481
+ generation: started.generation,
482
+ activeAtStart: started.activeAtStart
483
+ };
394
484
  const current = yield* Stream.runCollect(ledger.scanNonterminal);
485
+ const selectionTime = yield* Clock.currentTimeMillis;
486
+ yield* runTransaction("read Thread recovery deadlines", async () => {
487
+ for (const threadId of new Set(current.map((row) => row.threadId))) {
488
+ if (recovery.loaded.has(threadId)) continue;
489
+ const encoded = await ctx.storage.get(recoveryFaultKey(threadId));
490
+ if (encoded !== void 0) recovery.faults.set(threadId, decodeRecoveryFault(threadId, encoded));
491
+ recovery.loaded.add(threadId);
492
+ }
493
+ });
494
+ const reports = recovery.reports;
495
+ const recoveryFaults = recovery.faults;
496
+ const waiting = (row) => !recovery.pending.has(row.threadId) && !recoveryFaults.has(row.threadId) && stableExternalWait(row, reports);
395
497
  const heads = /* @__PURE__ */ new Map();
396
498
  for (const row of current) {
397
- if (row.state === "unknown" && stableExternalWait(row, reports)) continue;
499
+ if (row.state === "unknown" && waiting(row)) continue;
398
500
  if (!heads.has(row.threadId)) heads.set(row.threadId, row);
399
501
  }
400
- const eligible = [...heads.values()].filter((head) => !stableExternalWait(head, reports)).map((head) => head.threadId).sort();
401
- const selectionTime = yield* Clock.currentTimeMillis;
502
+ const stopping = /* @__PURE__ */ new Set();
503
+ for (const head of heads.values()) {
504
+ if (head.state !== "ready" || recovery.pending.has(head.threadId) || recoveryFaults.has(head.threadId)) continue;
505
+ const intent = yield* isolateRecovery(ledger.readAbortIntent(AbortIntentRequest.make({ submissionId: head.submissionId })), {
506
+ timeout: recoveryConfig.recoveryTimeout,
507
+ phase: () => "recovery",
508
+ operation: "read abort intent"
509
+ });
510
+ if (Result.isSuccess(intent)) {
511
+ if (intent.success !== void 0) stopping.add(head.threadId);
512
+ } else {
513
+ const faults = yield* recordRecoveryStatus(RecoverySweepResult.make({
514
+ reports: [],
515
+ blocked: [RecoveryBlocked.make({
516
+ threadId: head.threadId,
517
+ failure: intent.failure
518
+ })]
519
+ }));
520
+ for (const [threadId, fault] of faults) recoveryFaults.set(threadId, fault);
521
+ recovery.recovered++;
522
+ }
523
+ }
524
+ const eligible = [...heads.values()].filter((head) => !recovery.pending.has(head.threadId) && !stopping.has(head.threadId) && !recoveryFaults.has(head.threadId) && !waiting(head) && (head.state === "ready" || reports.has(head.submissionId))).map((head) => head.threadId).sort();
402
525
  yield* failpoint.hit("maintenance:select:before");
403
526
  const selection = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
404
527
  const { state } = await readMaintenanceState(transaction);
405
528
  const retries = (state.bindingRetries ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
406
529
  const runnable = eligible.filter((threadId) => !retries.some((retry) => retry.threadId === threadId && retry.notBefore > selectionTime));
407
- const next = runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0];
530
+ const next = dispatch ? runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0] : void 0;
408
531
  if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
409
532
  ...state,
410
533
  lastServedThreadId: next
411
534
  })));
535
+ const backlog = recovery.started ? [] : [...heads.keys()].filter((threadId) => {
536
+ const fault = recoveryFaults.get(threadId);
537
+ return !eligible.includes(threadId) && (fault === void 0 || fault.retryAt <= selectionTime);
538
+ });
539
+ const after = backlog.filter((threadId) => state.lastRecoveredThreadId === void 0 || threadId > state.lastRecoveredThreadId);
540
+ const before = backlog.filter((threadId) => state.lastRecoveredThreadId !== void 0 && threadId <= state.lastRecoveredThreadId);
412
541
  return {
413
542
  selected: next,
414
- retries
543
+ retries,
544
+ backlog: [...after, ...before]
415
545
  };
416
546
  }));
417
547
  yield* failpoint.hit("maintenance:select:after");
548
+ if (!recovery.started) {
549
+ recovery.started = true;
550
+ recovery.needsCheckpoint = selection.backlog.length > 0;
551
+ for (const threadId of selection.backlog) recovery.pending.add(threadId);
552
+ yield* Deferred.succeed(recovery.queue, selection.backlog);
553
+ }
418
554
  const selected = selection.selected === void 0 ? void 0 : heads.get(selection.selected);
555
+ if (selected !== void 0) yield* recoverThread(selected.threadId, recovery);
419
556
  let retries = selection.retries;
420
557
  let bindingFailure;
421
- const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag("BindingUnavailable", (failure) => {
558
+ const settlement = selected === void 0 || recoveryFaults.has(selected.threadId) ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag("BindingUnavailable", (failure) => {
422
559
  bindingFailure = failure;
423
560
  return Effect.succeed(Option.none());
424
561
  }));
@@ -456,22 +593,22 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
456
593
  const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
457
594
  const waitingHeads = /* @__PURE__ */ new Map();
458
595
  const autonomous = remaining.some((snapshot) => {
459
- if (snapshot.state === "unknown" && stableExternalWait(snapshot, reports)) return false;
596
+ if (snapshot.state === "unknown" && waiting(snapshot)) return false;
460
597
  const headWaiting = waitingHeads.get(snapshot.threadId);
461
- if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, stableExternalWait(snapshot, reports));
598
+ if (headWaiting === void 0) waitingHeads.set(snapshot.threadId, waiting(snapshot));
462
599
  if (headWaiting === true && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
463
- return !stableExternalWait(snapshot, reports);
600
+ return !waiting(snapshot);
464
601
  });
465
- const progressed = Option.isSome(settlement) || recovered.some((report) => report.disposition === "repaired");
602
+ const progressed = Option.isSome(settlement) || recovery.repaired;
466
603
  const now = yield* Clock.currentTimeMillis;
467
604
  const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
468
- const nextEligible = eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now);
469
- const bindingDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
470
- const delay = Math.max(ordinaryDelay, bindingDelay);
605
+ const nextEligible = [...recoveryFaults.values()].map((fault) => fault.retryAt).concat(eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now));
606
+ const retryDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
607
+ const delay = Math.max(ordinaryDelay, retryDelay);
471
608
  yield* failpoint.hit("maintenance:checkpoint:before");
472
609
  const nextAttemptAt = yield* mutations.withSnapshot((active) => runTransaction("checkpoint native maintenance", () => ctx.storage.transaction(async (transaction) => {
473
610
  const { state } = await readMaintenanceState(transaction);
474
- const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
611
+ const processed = autonomous || observation.activeAtStart > 0 || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > observation.generation ? state.processed : observation.generation;
475
612
  const next = ThreadMaintenanceState.make({
476
613
  ...Struct.omit(state, ["retry"]),
477
614
  processed,
@@ -491,7 +628,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
491
628
  yield* failpoint.hit("maintenance:checkpoint:after");
492
629
  return {
493
630
  phase: "actionable",
494
- recovered: recovered.length,
495
631
  settled: Option.isSome(settlement) ? 1 : 0,
496
632
  nonterminal: remaining.length,
497
633
  nextAttemptAt
@@ -499,8 +635,35 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
499
635
  });
500
636
  const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
501
637
  const notified = (yield* Stream.toPull(wakes.wakes)).pipe(Effect.asVoid, Effect.catch(() => Effect.never));
502
- let started = yield* beginNative(observed);
638
+ const started = yield* beginNative(observed);
503
639
  const auxiliaryScope = yield* Effect.acquireRelease(Scope.make("parallel"), (scope, exit) => Scope.close(scope, exit));
640
+ const recovery = {
641
+ queue: yield* Deferred.make(),
642
+ pending: /* @__PURE__ */ new Set(),
643
+ loaded: /* @__PURE__ */ new Set(),
644
+ reports: /* @__PURE__ */ new Map(),
645
+ faults: /* @__PURE__ */ new Map(),
646
+ started: false,
647
+ needsCheckpoint: false,
648
+ recovered: 0,
649
+ repaired: false
650
+ };
651
+ const recoveryFiber = yield* Effect.forkIn(Effect.gen(function* () {
652
+ for (const threadId of yield* Deferred.await(recovery.queue)) {
653
+ yield* failpoint.hit("maintenance:select:before");
654
+ yield* runTransaction("select old recovery lane", () => ctx.storage.transaction(async (transaction) => {
655
+ const { state } = await readMaintenanceState(transaction);
656
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
657
+ ...state,
658
+ lastRecoveredThreadId: threadId
659
+ })));
660
+ }));
661
+ yield* failpoint.hit("maintenance:select:after");
662
+ yield* recoverThread(threadId, recovery);
663
+ recovery.pending.delete(threadId);
664
+ yield* wakes.notify(threadId);
665
+ }
666
+ }), auxiliaryScope);
504
667
  const dispatchClosed = yield* Deferred.make();
505
668
  const stopDispatch = Deferred.await(dispatchClosed);
506
669
  const deliveryFiber = yield* Effect.forkIn(Scope.provide(auxiliaryScope)(messages.drainUntil(stopDispatch, dispatchUntil)), auxiliaryScope);
@@ -513,35 +676,61 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
513
676
  yield* host.drainUntil(stopDispatch, dispatchUntil);
514
677
  })), auxiliaryScope);
515
678
  const backfill = yield* Effect.forkIn(drainDue.pipe(Effect.provideService(ThreadProjectionMaintenance, projection), Effect.timeoutOption(config.projectionDispatchTimeoutMillis)), auxiliaryScope);
516
- let result = yield* advance(started, yieldAfter, observed);
679
+ let result = yield* advance(started, yieldAfter, observed, recovery);
680
+ recovery.started = true;
681
+ yield* Deferred.succeed(recovery.queue, []);
517
682
  let phase = result.phase;
518
- let recovered = result.recovered;
519
683
  let settled = result.settled;
520
684
  observed.nativeOnly = false;
521
- yield* Deferred.succeed(dispatchClosed, void 0);
522
- const remaining = Math.max(1, DateTime.toEpochMillis(dispatchUntil) - (yield* Clock.currentTimeMillis));
523
- const hostJoin = yield* Effect.forkIn(Fiber.join(hostFiber).pipe(Effect.timeoutOption(Math.min(host.dispatchTimeoutMillis, remaining)), Effect.tap((outcome) => Effect.annotateCurrentSpan({ "host.timedOut": Option.isNone(outcome) }))), auxiliaryScope);
685
+ const hostJoin = yield* Effect.forkIn(Effect.gen(function* () {
686
+ yield* stopDispatch;
687
+ const remaining = Math.max(1, DateTime.toEpochMillis(dispatchUntil) - (yield* Clock.currentTimeMillis));
688
+ const outcome = yield* Fiber.join(hostFiber).pipe(Effect.timeoutOption(Math.min(host.dispatchTimeoutMillis, remaining)));
689
+ yield* Effect.annotateCurrentSpan({ "host.timedOut": Option.isNone(outcome) });
690
+ }), auxiliaryScope);
524
691
  const retired = yield* Effect.forkChild(Fiber.joinAll([
525
692
  deliveryFiber,
526
693
  hostJoin,
527
- backfill
694
+ backfill,
695
+ recoveryFiber
528
696
  ]));
529
- const auxiliaryPending = () => deliveryFiber.pollUnsafe() === void 0 || hostFiber.pollUnsafe() === void 0 || backfill.pollUnsafe() === void 0;
530
- while (retired.pollUnsafe() === void 0 && auxiliaryPending()) {
697
+ const auxiliaryPending = () => deliveryFiber.pollUnsafe() === void 0 || hostFiber.pollUnsafe() === void 0 || backfill.pollUnsafe() === void 0 || recoveryFiber.pollUnsafe() === void 0;
698
+ while (true) {
699
+ const recoveryFinished = recoveryFiber.pollUnsafe() !== void 0;
700
+ if (recoveryFinished) {
701
+ if (recovery.needsCheckpoint) {
702
+ yield* Fiber.join(recoveryFiber);
703
+ result = yield* advance(started, yieldAfter, observed, recovery, settled === 0);
704
+ if (result.phase === "actionable") phase = "actionable";
705
+ settled += result.settled;
706
+ recovery.needsCheckpoint = false;
707
+ }
708
+ yield* Deferred.succeed(dispatchClosed, void 0);
709
+ }
710
+ if (retired.pollUnsafe() !== void 0 || !auxiliaryPending()) break;
531
711
  const now = yield* Clock.currentTimeMillis;
532
712
  const until = DateTime.toEpochMillis(yieldAfter);
533
713
  if (now >= until) break;
534
714
  const next = Math.min(result.nextAttemptAt ?? Infinity, now + config.wakeScanInterval, until);
535
- if (!(yield* Effect.raceFirst(Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as(true)), Fiber.await(retired).pipe(Effect.as(false)))) || retired.pollUnsafe() !== void 0 || !auxiliaryPending()) break;
715
+ const recoveryDone = recoveryFinished ? Effect.never : Fiber.await(recoveryFiber);
716
+ const ready = yield* Effect.raceFirst(Effect.raceFirst(notified, Effect.sleep(Math.max(0, next - now))).pipe(Effect.as("native")), Effect.raceFirst(recoveryDone.pipe(Effect.as("recovery")), Fiber.await(retired).pipe(Effect.as("retired"))));
717
+ if (ready === "recovery") continue;
718
+ if (ready === "retired" || retired.pollUnsafe() !== void 0) break;
719
+ if (!auxiliaryPending()) continue;
536
720
  if ((yield* Clock.currentTimeMillis) >= until) break;
537
- started = yield* beginNative(observed);
538
- result = yield* advance(started, yieldAfter, observed);
721
+ const awakened = yield* beginNative(observed);
722
+ result = yield* advance(awakened, yieldAfter, observed, recovery);
539
723
  if (result.phase === "actionable") phase = "actionable";
540
- recovered += result.recovered;
541
724
  settled += result.settled;
542
725
  observed.nativeOnly = false;
543
726
  }
727
+ yield* Deferred.succeed(dispatchClosed, void 0);
544
728
  yield* Fiber.join(retired);
729
+ if (recovery.needsCheckpoint) {
730
+ result = yield* advance(started, yieldAfter, observed, recovery, false);
731
+ if (result.phase === "actionable") phase = "actionable";
732
+ settled += result.settled;
733
+ }
545
734
  yield* Scope.close(auxiliaryScope, Exit.void);
546
735
  yield* failpoint.hit("maintenance:finish:before");
547
736
  const disposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
@@ -562,7 +751,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
562
751
  yield* failpoint.hit("maintenance:finish:after");
563
752
  const report = MaintenancePassReport.make({
564
753
  phase,
565
- recovered,
754
+ recovered: recovery.recovered,
566
755
  settled,
567
756
  nonterminal: result.nonterminal,
568
757
  alarm: disposition
@@ -591,11 +780,12 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
591
780
  })
592
781
  })),
593
782
  ensureAlarm: mutations.withSnapshot(() => ensureAlarm()),
783
+ recoveryStatus,
594
784
  withMutation: (body) => mutations.withMutation(body.pipe(Effect.tap(() => publishCommitted.pipe(Effect.provideService(ThreadPublication, publication)))))
595
785
  });
596
786
  }));
597
787
  };
598
788
  //#endregion
599
- export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, publishCommitted, Alarm_exports as t };
789
+ export { DurableAlarmError, DurableAlarmService, MaintenancePassReport, ThreadHostMaintenance, ThreadMaintenance, ThreadMaintenanceFailpoint, ThreadMessageDelivery, ThreadMutationGate, ThreadPublication, ThreadRecoveryFault, publishCommitted, Alarm_exports as t };
600
790
 
601
791
  //# sourceMappingURL=Alarm.mjs.map