@effect-agent/platform-cloudflare 0.1.0-beta.95 → 0.1.0-beta.96

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
@@ -1,5 +1,5 @@
1
1
  import { DurableObjectContext } from "./CloudflareBindings.mjs";
2
- import { s as CloudflareDurableRuntimeConfig } from "./CloudflareConfig-D472JOQA.mjs";
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
5
  import { DurableAgentRuntime, DurableWorkerFailure } from "effect-agent/durable-agent-runtime";
@@ -75,7 +75,7 @@ declare const MaintenancePassReport_base: Schema.Class<MaintenancePassReport, Sc
75
75
  /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
76
76
  declare class MaintenancePassReport extends MaintenancePassReport_base {}
77
77
  /** Fault boundaries around every maintenance-owned durable mutation. */
78
- 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:finish:before" | "maintenance:finish:after";
78
+ 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:finish:before" | "maintenance:finish:after";
79
79
  type ThreadMaintenanceFailpointHandler = (location: ThreadMaintenanceFailpointLocation) => Effect.Effect<void>;
80
80
  declare const ThreadMaintenanceFailpoint_base: Context.ServiceClass<ThreadMaintenanceFailpoint, "@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint", {
81
81
  readonly hit: ThreadMaintenanceFailpointHandler;
@@ -167,7 +167,7 @@ declare class ThreadMutationGate extends ThreadMutationGate_base {
167
167
  }
168
168
  type MaintenancePassFailure = DurableWorkerFailure | DurableBindingFailure | DurableAlarmError | ThreadProjectionError;
169
169
  declare const ThreadMaintenance_base: Context.ServiceClass<ThreadMaintenance, "@effect-agent/platform-cloudflare/ThreadMaintenance", {
170
- /** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */
170
+ /** One idempotent pass; failures propagate after durably scheduling bounded recovery. */
171
171
  readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
172
172
  /**
173
173
  * Constructor gate: initialize/inspect only the O(1) maintenance record and ensure a dirty
package/dist/Alarm.mjs CHANGED
@@ -2,10 +2,10 @@ 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 } from "effect";
5
+ import { Cause, Clock, Context, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Random, Ref, Schema, Scope, Semaphore, Stream, Struct } from "effect";
6
6
  import "effect-agent/agent-registration";
7
7
  import { DurableAgentRuntime } from "effect-agent/durable-agent-runtime";
8
- import { ThreadId } from "effect-agent/identifiers";
8
+ import { SubmissionId, ThreadId } from "effect-agent/identifiers";
9
9
  import { SubmissionLedger } from "effect-agent/submission-ledger";
10
10
  import { ThreadProjectionMaintenance, drainDue } from "effect-agent/thread-projection-maintenance";
11
11
  import { SqlClient } from "effect/unstable/sql/SqlClient";
@@ -147,6 +147,19 @@ const publishCommitted = Effect.gen(function* () {
147
147
  yield* publication.invalidate.pipe(Effect.andThen(publication.drain));
148
148
  }).pipe(Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt : Effect.logError("Thread publication deferred after source commit", cause)));
149
149
  const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
150
+ var BindingRetry = class extends Schema.Class("BindingRetry")({
151
+ threadId: ThreadId,
152
+ submissionId: SubmissionId,
153
+ attempts: Schema.Natural,
154
+ notBefore: Schema.Finite,
155
+ reportedAt: Schema.Finite
156
+ }) {};
157
+ var MaintenanceRetry = class extends Schema.Class("MaintenanceRetry")({
158
+ generation: MaintenanceGeneration,
159
+ notBefore: Schema.Finite,
160
+ nativeOnly: Schema.Boolean,
161
+ stalls: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(30))
162
+ }) {};
150
163
  /** Versioned, platform-private maintenance state stored through Durable Object KV. */
151
164
  var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
152
165
  schemaVersion: Schema.Literal(1),
@@ -154,7 +167,10 @@ var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-
154
167
  processed: MaintenanceGeneration,
155
168
  nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
156
169
  /** One physical-owner cursor; old single-lane records need no conversion. */
157
- lastServedThreadId: Schema.optionalKey(ThreadId)
170
+ lastServedThreadId: Schema.optionalKey(ThreadId),
171
+ bindingRetries: Schema.optionalKey(Schema.Array(BindingRetry)),
172
+ /** Absent on older records. A newer mutation makes this retry obsolete. */
173
+ retry: Schema.optionalKey(MaintenanceRetry)
158
174
  }) {};
159
175
  const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
160
176
  const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
@@ -258,11 +274,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
258
274
  const config = yield* CloudflareDurableRuntimeConfig;
259
275
  const { ctx } = yield* DurableObjectContext;
260
276
  const failpoint = yield* ThreadMaintenanceFailpoint;
261
- /**
262
- * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
263
- * restarts at zero and merely re-arms sooner than a long-lived one would have.
264
- */
265
- const stalls = yield* Ref.make(0);
266
277
  const mutations = yield* ThreadMutationGate;
267
278
  const publication = yield* ThreadPublication;
268
279
  const projection = yield* ThreadProjectionMaintenance;
@@ -278,48 +289,74 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
278
289
  const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
279
290
  yield* failpoint.hit("maintenance:ensure:before");
280
291
  const now = yield* Clock.currentTimeMillis;
281
- yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
292
+ const retry = yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
282
293
  const { state, initialized } = await readMaintenanceState(transaction);
283
294
  if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
284
- if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
295
+ if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, state.retry?.generation === state.dirty ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : now + config.wakeScanInterval);
296
+ return state.retry?.generation === state.dirty ? state.retry : void 0;
285
297
  }));
286
298
  const deadline = yield* pendingDeadline;
287
- if (Option.isSome(deadline)) yield* runTransaction("ensure publication alarm", () => ctx.storage.transaction((transaction) => ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, deadline.value))));
299
+ if (Option.isSome(deadline)) yield* runTransaction("ensure publication alarm", () => ctx.storage.transaction((transaction) => ensureTransactionAlarmBy(transaction, Math.max(now + minimumAlarmDelay, deadline.value <= now && retry !== void 0 && !retry.nativeOnly ? Math.max(deadline.value, retry.notBefore) : deadline.value))));
288
300
  yield* failpoint.hit("maintenance:ensure:after");
289
301
  });
290
- const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
302
+ const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* (observed) {
291
303
  yield* failpoint.hit("maintenance:begin:before");
292
304
  const now = yield* Clock.currentTimeMillis;
293
305
  const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
294
306
  const { state, initialized } = await readMaintenanceState(transaction);
307
+ observed.generation = state.dirty;
295
308
  if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
296
- if (state.processed >= state.dirty) {
297
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
298
- return {
299
- _tag: "CaughtUp",
300
- nonterminal: state.nonterminal
301
- };
302
- }
309
+ const retryAt = state.retry?.generation === state.dirty ? state.retry.notBefore : 0;
303
310
  await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
311
+ if (state.processed >= state.dirty || retryAt > now) return {
312
+ _tag: "CaughtUp",
313
+ nonterminal: state.nonterminal
314
+ };
304
315
  return {
305
316
  _tag: "Actionable",
306
317
  generation: state.dirty,
307
- nonterminal: state.nonterminal
318
+ nonterminal: state.nonterminal,
319
+ stalls: state.retry?.generation === state.dirty ? state.retry.stalls : 0
308
320
  };
309
321
  }));
310
322
  yield* failpoint.hit("maintenance:begin:after");
311
323
  return result;
312
324
  });
313
- const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed) {
314
- const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
315
- if (progressed) return config.alarmBackoffBase;
316
- const exponent = Math.min(priorStalls, 30);
317
- const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
318
- const jitter = yield* Random.next;
319
- const jittered = Math.ceil(backoff / 2 + backoff / 2 * jitter);
320
- return Math.min(jittered, config.wakeScanInterval);
325
+ const backoffDelay = (priorStalls, jitter) => {
326
+ const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** Math.min(priorStalls, 30));
327
+ return Math.ceil(backoff / 2 + backoff / 2 * jitter);
328
+ };
329
+ const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed, priorStalls) {
330
+ return progressed ? config.alarmBackoffBase : backoffDelay(priorStalls, yield* Random.next);
321
331
  });
322
- const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil) {
332
+ const rearmFailure = Effect.fn("ThreadMaintenance.rearmFailure")(function* (observed) {
333
+ const { generation, nativeOnly } = observed;
334
+ if (generation === void 0) return;
335
+ yield* failpoint.hit("maintenance:retry:before");
336
+ yield* mutations.withSnapshot((active) => Effect.gen(function* () {
337
+ const deadline = yield* pendingDeadline.pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
338
+ const now = yield* Clock.currentTimeMillis;
339
+ const jitter = yield* Random.next;
340
+ yield* runTransaction("back off failed maintenance", () => ctx.storage.transaction(async (transaction) => {
341
+ const { state } = await readMaintenanceState(transaction);
342
+ const previous = state.retry?.generation === generation ? state.retry : void 0;
343
+ const retry = MaintenanceRetry.make({
344
+ generation,
345
+ notBefore: Math.max(previous?.notBefore ?? 0, now + backoffDelay(previous?.stalls ?? 0, jitter)),
346
+ nativeOnly,
347
+ stalls: Math.min(30, (previous?.stalls ?? 0) + 1)
348
+ });
349
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
350
+ ...state,
351
+ retry
352
+ })));
353
+ const nativeDeadline = active > 0 || state.dirty !== generation ? now + minimumAlarmDelay : retry.notBefore;
354
+ await transaction.setAlarm(Math.max(now + minimumAlarmDelay, Option.isSome(deadline) && (nativeOnly || deadline.value > now) ? Math.min(nativeDeadline, deadline.value) : nativeDeadline));
355
+ }));
356
+ }));
357
+ yield* failpoint.hit("maintenance:retry:after");
358
+ });
359
+ const pass = Effect.fn("ThreadMaintenance.pass")(function* (yieldAfter, dispatchUntil, observed) {
323
360
  const annotate = (report) => Effect.annotateCurrentSpan({
324
361
  phase: report.phase,
325
362
  recovered: report.recovered,
@@ -328,7 +365,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
328
365
  alarm: report.alarm
329
366
  }).pipe(Effect.as(report));
330
367
  const started = yield* mutations.withSnapshot((activeAtStart) => Effect.gen(function* () {
331
- const generation = yield* beginPass();
368
+ const generation = yield* beginPass(observed);
332
369
  if (generation._tag === "Actionable" && activeAtStart === 0) yield* publication.prepareGeneration(generation.generation);
333
370
  return {
334
371
  ...generation,
@@ -370,7 +407,7 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
370
407
  const now = yield* Clock.currentTimeMillis;
371
408
  return yield* runTransaction("finish publication pass", () => ctx.storage.transaction(async (transaction) => {
372
409
  const { state } = await readMaintenanceState(transaction);
373
- const nativeDeadline = active > 0 || state.dirty > state.processed ? now + config.wakeScanInterval : Infinity;
410
+ const nativeDeadline = active > 0 || state.dirty > state.processed ? state.retry?.generation === state.dirty && active === 0 ? Math.max(now + minimumAlarmDelay, state.retry.notBefore) : now + config.wakeScanInterval : Infinity;
374
411
  const next = Option.isSome(latest) ? Math.min(nativeDeadline, latest.value) : nativeDeadline;
375
412
  if (Number.isFinite(next)) {
376
413
  await transaction.setAlarm(Math.max(now + minimumAlarmDelay, next));
@@ -389,27 +426,69 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
389
426
  alarm: disposition
390
427
  }));
391
428
  }
429
+ observed.nativeOnly = true;
392
430
  const recovered = yield* runtime.runRecovery;
393
431
  const reports = new Map(recovered.map((report) => [report.submissionId, report]));
394
432
  const current = yield* Stream.runCollect(ledger.scanNonterminal);
395
433
  const heads = /* @__PURE__ */ new Map();
396
434
  for (const row of current) if (!heads.has(row.threadId)) heads.set(row.threadId, row);
397
435
  const eligible = [...heads.values()].filter((head) => !stableExternalWait(head, reports)).map((head) => head.threadId).sort();
398
- let selected = eligible[0];
399
- if (heads.size > 1 && selected !== void 0) {
400
- yield* failpoint.hit("maintenance:select:before");
401
- selected = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
402
- const { state } = await readMaintenanceState(transaction);
403
- const next = eligible.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? eligible[0];
404
- if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
405
- ...state,
406
- lastServedThreadId: next
407
- })));
408
- return next;
409
- }));
410
- yield* failpoint.hit("maintenance:select:after");
436
+ const selectionTime = yield* Clock.currentTimeMillis;
437
+ yield* failpoint.hit("maintenance:select:before");
438
+ const selection = yield* runTransaction("select maintenance lane", () => ctx.storage.transaction(async (transaction) => {
439
+ const { state } = await readMaintenanceState(transaction);
440
+ const retries = (state.bindingRetries ?? []).filter((retry) => heads.get(retry.threadId)?.submissionId === retry.submissionId);
441
+ const runnable = eligible.filter((threadId) => !retries.some((retry) => retry.threadId === threadId && retry.notBefore > selectionTime));
442
+ const next = runnable.find((threadId) => state.lastServedThreadId === void 0 || threadId > state.lastServedThreadId) ?? runnable[0];
443
+ if (next !== void 0) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
444
+ ...state,
445
+ lastServedThreadId: next
446
+ })));
447
+ return {
448
+ selected: next,
449
+ retries
450
+ };
451
+ }));
452
+ yield* failpoint.hit("maintenance:select:after");
453
+ const selected = selection.selected === void 0 ? void 0 : heads.get(selection.selected);
454
+ let retries = selection.retries;
455
+ let bindingFailure;
456
+ const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected.threadId, { yieldAfter }).pipe(Effect.catchTag(["BindingUnavailable", "BindingDigestMismatch"], (failure) => {
457
+ bindingFailure = failure;
458
+ return Effect.succeed(Option.none());
459
+ }));
460
+ if (selected !== void 0) {
461
+ const previous = retries.find((retry) => retry.submissionId === selected.submissionId);
462
+ let retry;
463
+ let reportBindingFailure = false;
464
+ if (bindingFailure !== void 0) {
465
+ const now = yield* Clock.currentTimeMillis;
466
+ const attempts = Math.min(30, (previous?.attempts ?? 0) + 1);
467
+ reportBindingFailure = previous === void 0 || now - previous.reportedAt >= 9e5;
468
+ retry = BindingRetry.make({
469
+ threadId: selected.threadId,
470
+ submissionId: selected.submissionId,
471
+ attempts,
472
+ notBefore: now + Math.min(6e4, 5e3 * 2 ** (attempts - 1)),
473
+ reportedAt: reportBindingFailure ? now : previous?.reportedAt ?? now
474
+ });
475
+ }
476
+ if (retry !== void 0 || previous !== void 0) {
477
+ yield* failpoint.hit("maintenance:binding-retry:before");
478
+ retries = yield* runTransaction("record submission binding retry", () => ctx.storage.transaction(async (transaction) => {
479
+ const { state } = await readMaintenanceState(transaction);
480
+ const bindingRetries = [...(state.bindingRetries ?? []).filter((entry) => entry.submissionId !== selected.submissionId), ...retry === void 0 ? [] : [retry]];
481
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(ThreadMaintenanceState.make({
482
+ ...state,
483
+ bindingRetries
484
+ })));
485
+ return bindingRetries;
486
+ }));
487
+ yield* failpoint.hit("maintenance:binding-retry:after");
488
+ }
489
+ if (bindingFailure !== void 0) yield* reportBindingFailure ? Effect.logError("Thread awaits a compatible binding; original work remains pending", Cause.fail(bindingFailure)) : Effect.logDebug("Thread binding retry remains pending", Cause.fail(bindingFailure));
411
490
  }
412
- const settlement = selected === void 0 ? Option.none() : yield* runtime.processThreadHead(selected, { yieldAfter });
491
+ observed.nativeOnly = false;
413
492
  yield* finishAuxiliary;
414
493
  const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
415
494
  const waitingHeads = /* @__PURE__ */ new Map();
@@ -420,8 +499,11 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
420
499
  return !stableExternalWait(snapshot, reports);
421
500
  });
422
501
  const progressed = Option.isSome(settlement) || recovered.some((report) => report.disposition === "repaired");
423
- const delay = autonomous ? yield* rearmDelay(progressed) : 0;
424
502
  const now = yield* Clock.currentTimeMillis;
503
+ const ordinaryDelay = autonomous ? yield* rearmDelay(progressed, started.stalls) : 0;
504
+ const nextEligible = eligible.map((threadId) => retries.find((retry) => retry.submissionId === heads.get(threadId)?.submissionId)?.notBefore ?? now);
505
+ const bindingDelay = nextEligible.length === 0 ? 0 : Math.max(0, Math.min(...nextEligible) - now);
506
+ const delay = Math.max(ordinaryDelay, bindingDelay);
425
507
  yield* failpoint.hit("maintenance:finish:before");
426
508
  const alarmDisposition = yield* mutations.withSnapshot((active) => Effect.gen(function* () {
427
509
  const publicationDeadline = yield* pendingDeadline;
@@ -429,13 +511,21 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
429
511
  const { state } = await readMaintenanceState(transaction);
430
512
  const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
431
513
  const next = ThreadMaintenanceState.make({
432
- ...state,
514
+ ...Struct.omit(state, ["retry"]),
433
515
  processed,
434
- nonterminal: remaining.length
516
+ nonterminal: remaining.length,
517
+ bindingRetries: (state.bindingRetries ?? []).filter((retry) => remaining.some((row) => row.submissionId === retry.submissionId)),
518
+ ...autonomous && !progressed ? { retry: MaintenanceRetry.make({
519
+ generation: started.generation,
520
+ notBefore: now + delay,
521
+ nativeOnly: true,
522
+ stalls: Math.min(30, started.stalls + 1)
523
+ }) } : {}
435
524
  });
436
525
  await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
437
526
  if (autonomous) {
438
- await transaction.setAlarm(Option.isSome(publicationDeadline) ? Math.max(now + minimumAlarmDelay, Math.min(now + delay, publicationDeadline.value)) : now + delay);
527
+ const nativeDeadline = started.activeAtStart > 0 || active > 0 || state.dirty !== started.generation ? now + minimumAlarmDelay : now + delay;
528
+ await transaction.setAlarm(Option.isSome(publicationDeadline) ? Math.max(now + minimumAlarmDelay, Math.min(nativeDeadline, publicationDeadline.value)) : nativeDeadline);
439
529
  return "rearmed";
440
530
  }
441
531
  if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
@@ -451,7 +541,6 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
451
541
  }));
452
542
  }));
453
543
  yield* failpoint.hit("maintenance:finish:after");
454
- if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
455
544
  return yield* annotate(MaintenancePassReport.make({
456
545
  phase: "actionable",
457
546
  recovered: recovered.length,
@@ -465,7 +554,8 @@ var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effe
465
554
  const now = yield* Clock.currentTimeMillis;
466
555
  const yieldAfter = DateTime.makeUnsafe(now + 6e5);
467
556
  const dispatchUntil = DateTime.makeUnsafe(now + 84e4);
468
- return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(Effect.scoped(pass(yieldAfter, dispatchUntil))));
557
+ const observed = { nativeOnly: false };
558
+ return yield* alarm.withWakesDeferred(maintenancePassGate.withPermit(Effect.scoped(pass(yieldAfter, dispatchUntil, observed)).pipe(Effect.onErrorIf(() => true, () => rearmFailure(observed)))));
469
559
  }).pipe(Effect.timeoutOrElse({
470
560
  duration: "14 minutes",
471
561
  orElse: () => DurableAlarmError.make({