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

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.d.mts +119 -0
  2. package/dist/Alarm.mjs +317 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/{browser-rest-capture.d.mts → BrowserRestCapture.d.mts} +5 -4
  5. package/dist/{browser-rest-capture.mjs → BrowserRestCapture.mjs} +5 -4
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/{browser-rest-crawl.d.mts → BrowserRestCrawl.d.mts} +4 -3
  8. package/dist/{browser-rest-crawl.mjs → BrowserRestCrawl.mjs} +4 -3
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/{bindings-BrVEOkfe.d.mts → CloudflareBindings.d.mts} +7 -5
  11. package/dist/CloudflareBindings.mjs +98 -0
  12. package/dist/CloudflareBindings.mjs.map +1 -0
  13. package/dist/{browser-quick-action.mjs → CloudflareBrowser-1jeZx3_g.mjs} +19 -4
  14. package/dist/CloudflareBrowser-1jeZx3_g.mjs.map +1 -0
  15. package/dist/{browser-quick-action.d.mts → CloudflareBrowser-DJq53niJ.d.mts} +9 -18
  16. package/dist/CloudflareBrowser.d.mts +2 -0
  17. package/dist/CloudflareBrowser.mjs +2 -0
  18. package/dist/CloudflareCodeMode.d.mts +44 -0
  19. package/dist/CloudflareCodeMode.mjs +618 -0
  20. package/dist/CloudflareCodeMode.mjs.map +1 -0
  21. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  22. package/dist/CloudflareConfig.d.mts +2 -0
  23. package/dist/CloudflareConfig.mjs +122 -0
  24. package/dist/CloudflareConfig.mjs.map +1 -0
  25. package/dist/CloudflareMemory.d.mts +140 -0
  26. package/dist/CloudflareMemory.mjs +168 -0
  27. package/dist/CloudflareMemory.mjs.map +1 -0
  28. package/dist/{scheduling.d.mts → CloudflareScheduling.d.mts} +9 -6
  29. package/dist/{scheduling.mjs → CloudflareScheduling.mjs} +22 -8
  30. package/dist/CloudflareScheduling.mjs.map +1 -0
  31. package/dist/{subscriptions.d.mts → CloudflareSubscriptions.d.mts} +11 -6
  32. package/dist/{subscriptions.mjs → CloudflareSubscriptions.mjs} +26 -7
  33. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  34. package/dist/CloudflareThreadClient.d.mts +715 -0
  35. package/dist/CloudflareThreadClient.mjs +364 -0
  36. package/dist/CloudflareThreadClient.mjs.map +1 -0
  37. package/dist/{interactive-browser.d.mts → InteractiveBrowser.d.mts} +5 -4
  38. package/dist/{interactive-browser.mjs → InteractiveBrowser.mjs} +6 -4
  39. package/dist/InteractiveBrowser.mjs.map +1 -0
  40. package/dist/{protected-browser.d.mts → ProtectedBrowser.d.mts} +4 -3
  41. package/dist/{protected-browser.mjs → ProtectedBrowser.mjs} +4 -3
  42. package/dist/ProtectedBrowser.mjs.map +1 -0
  43. package/dist/ThreadObject-Ia94R_B1.d.mts +735 -0
  44. package/dist/ThreadObject-IrGuIO4a.mjs +575 -0
  45. package/dist/ThreadObject-IrGuIO4a.mjs.map +1 -0
  46. package/dist/ThreadObject.d.mts +2 -0
  47. package/dist/ThreadObject.mjs +3 -0
  48. package/dist/WakeScheduler.d.mts +23 -0
  49. package/dist/WakeScheduler.mjs +60 -0
  50. package/dist/WakeScheduler.mjs.map +1 -0
  51. package/dist/boundary-BguazVkh.mjs +42 -0
  52. package/dist/boundary-BguazVkh.mjs.map +1 -0
  53. package/dist/{browser-session-lifecycle-ZGgb3pnK.mjs → browser-session-lifecycle-CUbVfEgP.mjs} +2 -2
  54. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  55. package/dist/{browser-session-lifecycle-DqntvG-Y.d.mts → browser-session-lifecycle-DJpqLXy_.d.mts} +2 -2
  56. package/dist/index.d.mts +12 -1833
  57. package/dist/index.mjs +12 -1357
  58. package/dist/prepared-admission-DRBhl2Sp.mjs +63 -0
  59. package/dist/prepared-admission-DRBhl2Sp.mjs.map +1 -0
  60. package/package.json +1 -1
  61. package/src/{alarm.ts → Alarm.ts} +265 -272
  62. package/src/{browser-rest-capture.ts → BrowserRestCapture.ts} +3 -3
  63. package/src/{browser-rest-crawl.ts → BrowserRestCrawl.ts} +2 -2
  64. package/src/{bindings.ts → CloudflareBindings.ts} +2 -2
  65. package/src/CloudflareBrowser.ts +15 -0
  66. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +3 -3
  67. package/src/{config.ts → CloudflareConfig.ts} +3 -2
  68. package/src/{memory.ts → CloudflareMemory.ts} +15 -12
  69. package/src/{scheduling.ts → CloudflareScheduling.ts} +12 -11
  70. package/src/{subscriptions.ts → CloudflareSubscriptions.ts} +14 -12
  71. package/src/{client.ts → CloudflareThreadClient.ts} +27 -20
  72. package/src/{interactive-browser.ts → InteractiveBrowser.ts} +9 -5
  73. package/src/{thread-object.ts → ThreadObject.ts} +37 -29
  74. package/src/{wake-scheduler.ts → WakeScheduler.ts} +5 -5
  75. package/src/index.ts +11 -30
  76. package/src/{browser-quick-action.ts → internal/browser-quick-action.ts} +10 -8
  77. package/src/{layers.ts → internal/layers.ts} +42 -33
  78. package/src/{prepared-admission.ts → internal/prepared-admission.ts} +7 -7
  79. package/src/{transport.ts → internal/transport.ts} +5 -2
  80. package/src/protected-browser/binding.ts +3 -2
  81. package/src/protected-browser/native.ts +2 -2
  82. package/src/protected-browser/policy.ts +2 -2
  83. package/dist/browser-quick-action.mjs.map +0 -1
  84. package/dist/browser-rest-capture.mjs.map +0 -1
  85. package/dist/browser-rest-crawl.mjs.map +0 -1
  86. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +0 -1
  87. package/dist/index.mjs.map +0 -1
  88. package/dist/interactive-browser.mjs.map +0 -1
  89. package/dist/prepared-admission-BKp_Upw2.mjs +0 -890
  90. package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
  91. package/dist/protected-browser.mjs.map +0 -1
  92. package/dist/scheduling.mjs.map +0 -1
  93. package/dist/subscriptions.mjs.map +0 -1
  94. /package/src/{protected-browser.ts → ProtectedBrowser.ts} +0 -0
  95. /package/src/{boundary.ts → internal/boundary.ts} +0 -0
  96. /package/src/{browser-session-lifecycle.ts → internal/browser-session-lifecycle.ts} +0 -0
  97. /package/src/{progress-wait.ts → internal/progress-wait.ts} +0 -0
@@ -1,12 +1,10 @@
1
+ import { type DurableBindingFailure } from "@effect-agent/thread/AgentRegistration";
1
2
  import {
2
- type ResolvedBinding,
3
3
  DurableAgentRuntime,
4
- SubmissionLedger,
5
- type DurableBindingFailure,
6
4
  type DurableWorkerFailure,
7
5
  type RecoveryReport,
8
- type SubmissionSnapshot,
9
- } from "@effect-agent/thread";
6
+ } from "@effect-agent/thread/DurableAgentRuntime";
7
+ import { SubmissionLedger, type SubmissionSnapshot } from "@effect-agent/thread/SubmissionLedger";
10
8
  import {
11
9
  Clock,
12
10
  Context,
@@ -20,9 +18,9 @@ import {
20
18
  Stream,
21
19
  } from "effect";
22
20
 
23
- import { ThreadObjectIdentity, DurableObjectContext } from "./bindings.ts";
24
- import { safeCauseMessage } from "./boundary.ts";
25
- import { CloudflareDurableRuntimeConfig } from "./config.ts";
21
+ import { ThreadObjectIdentity, DurableObjectContext } from "./CloudflareBindings.ts";
22
+ import { CloudflareDurableRuntimeConfig } from "./CloudflareConfig.ts";
23
+ import { safeCauseMessage } from "./internal/boundary.ts";
26
24
 
27
25
  /**
28
26
  * The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
@@ -310,9 +308,7 @@ export class ThreadMaintenance extends Context.Service<
310
308
  ) => Effect.Effect<A, E | DurableAlarmError, R>;
311
309
  }
312
310
  >()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
313
- static readonly layer = (
314
- bindings: ReadonlyArray<ResolvedBinding>,
315
- ): Layer.Layer<
311
+ static readonly layer: Layer.Layer<
316
312
  ThreadMaintenance,
317
313
  never,
318
314
  | DurableAgentRuntime
@@ -322,288 +318,285 @@ export class ThreadMaintenance extends Context.Service<
322
318
  | CloudflareDurableRuntimeConfig
323
319
  | ThreadObjectIdentity
324
320
  | DurableObjectContext
325
- > =>
326
- Layer.effect(ThreadMaintenance)(
327
- Effect.gen(function* () {
328
- const runtime = yield* DurableAgentRuntime;
329
- const ledger = yield* SubmissionLedger;
330
- const alarm = yield* DurableAlarmService;
331
- const config = yield* CloudflareDurableRuntimeConfig;
332
- const identity = yield* ThreadObjectIdentity;
333
- const { ctx } = yield* DurableObjectContext;
334
- const failpoint = yield* ThreadMaintenanceFailpoint;
335
-
336
- /**
337
- * Consecutive no-progress passes an in-memory CACHE, not state: a fresh incarnation
338
- * restarts at zero and merely re-arms sooner than a long-lived one would have.
339
- */
340
- const stalls = yield* Ref.make(0);
341
- /**
342
- * Incarnation-local mutation count guarded with the generation transactions below. It is
343
- * deliberately not durable: after eviction every begun mutation has stopped, while its
344
- * pre-armed dirty generation remains durable for recovery. The short gate never spans the
345
- * caller's mutation or cross-Object I/O.
346
- */
347
- const activeMutations = yield* Ref.make(0);
348
- const generationGate = yield* Semaphore.make(1);
349
- // At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise
350
- // acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not
351
- // take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.
352
- const maintenancePassGate = yield* Semaphore.make(1);
353
- const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
354
-
355
- const runTransaction = <A>(
356
- operation: string,
357
- transaction: () => Promise<A>,
358
- ): Effect.Effect<A, DurableAlarmError> =>
359
- Effect.tryPromise({
360
- try: transaction,
361
- catch: alarmFailure(operation),
362
- });
363
-
364
- const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
365
- yield* failpoint.hit("maintenance:dirty:before");
366
- const now = yield* Clock.currentTimeMillis;
367
-
368
- yield* runTransaction("advance maintenance generation", () =>
369
- ctx.storage.transaction(async (transaction) => {
370
- const { state } = await readMaintenanceState(transaction);
371
-
372
- const next = ThreadMaintenanceState.make({
373
- ...state,
374
- dirty: state.dirty + 1n,
375
- });
376
-
377
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
378
- // The earliest configured retry bounds a newly actionable mutation without relying
379
- // on its best-effort immediate wake hint.
380
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
381
- }),
382
- );
383
- yield* failpoint.hit("maintenance:dirty:after");
384
- yield* Ref.update(activeMutations, (active) => active + 1);
321
+ > = Layer.effect(ThreadMaintenance)(
322
+ Effect.gen(function* () {
323
+ const runtime = yield* DurableAgentRuntime;
324
+ const ledger = yield* SubmissionLedger;
325
+ const alarm = yield* DurableAlarmService;
326
+ const config = yield* CloudflareDurableRuntimeConfig;
327
+ const identity = yield* ThreadObjectIdentity;
328
+ const { ctx } = yield* DurableObjectContext;
329
+ const failpoint = yield* ThreadMaintenanceFailpoint;
330
+
331
+ /**
332
+ * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
333
+ * restarts at zero and merely re-arms sooner than a long-lived one would have.
334
+ */
335
+ const stalls = yield* Ref.make(0);
336
+ /**
337
+ * Incarnation-local mutation count guarded with the generation transactions below. It is
338
+ * deliberately not durable: after eviction every begun mutation has stopped, while its
339
+ * pre-armed dirty generation remains durable for recovery. The short gate never spans the
340
+ * caller's mutation or cross-Object I/O.
341
+ */
342
+ const activeMutations = yield* Ref.make(0);
343
+ const generationGate = yield* Semaphore.make(1);
344
+ // At-least-once deliveries are idempotent, but overlapping pass bodies could otherwise
345
+ // acknowledge state while a sibling pass is still mutating it. Port/RPC mutations do not
346
+ // take this permit, so cross-Object I/O cannot deadlock the maintenance serialization.
347
+ const maintenancePassGate = yield* Semaphore.make(1);
348
+ const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
349
+
350
+ const runTransaction = <A>(
351
+ operation: string,
352
+ transaction: () => Promise<A>,
353
+ ): Effect.Effect<A, DurableAlarmError> =>
354
+ Effect.tryPromise({
355
+ try: transaction,
356
+ catch: alarmFailure(operation),
385
357
  });
386
358
 
387
- const endMutation = generationGate.withPermit(
388
- Ref.update(activeMutations, (active) => Math.max(0, active - 1)),
389
- );
359
+ const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
360
+ yield* failpoint.hit("maintenance:dirty:before");
361
+ const now = yield* Clock.currentTimeMillis;
390
362
 
391
- const withMutation = <A, E, R>(
392
- body: Effect.Effect<A, E, R>,
393
- ): Effect.Effect<A, E | DurableAlarmError, R> =>
394
- Effect.acquireUseRelease(
395
- generationGate.withPermit(beginMutation()),
396
- () =>
397
- failpoint.hit("maintenance:mutation:armed").pipe(
398
- Effect.andThen(body),
399
- Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
400
- ),
401
- () => endMutation,
402
- );
363
+ yield* runTransaction("advance maintenance generation", () =>
364
+ ctx.storage.transaction(async (transaction) => {
365
+ const { state } = await readMaintenanceState(transaction);
403
366
 
404
- const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
405
- yield* failpoint.hit("maintenance:ensure:before");
406
- const now = yield* Clock.currentTimeMillis;
367
+ const next = ThreadMaintenanceState.make({
368
+ ...state,
369
+ dirty: state.dirty + 1n,
370
+ });
407
371
 
408
- yield* runTransaction("ensure maintenance alarm", () =>
409
- ctx.storage.transaction(async (transaction) => {
410
- const { state, initialized } = await readMaintenanceState(transaction);
372
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
373
+ // The earliest configured retry bounds a newly actionable mutation without relying
374
+ // on its best-effort immediate wake hint.
375
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
376
+ }),
377
+ );
378
+ yield* failpoint.hit("maintenance:dirty:after");
379
+ yield* Ref.update(activeMutations, (active) => active + 1);
380
+ });
381
+
382
+ const endMutation = generationGate.withPermit(
383
+ Ref.update(activeMutations, (active) => Math.max(0, active - 1)),
384
+ );
385
+
386
+ const withMutation = <A, E, R>(
387
+ body: Effect.Effect<A, E, R>,
388
+ ): Effect.Effect<A, E | DurableAlarmError, R> =>
389
+ Effect.acquireUseRelease(
390
+ generationGate.withPermit(beginMutation()),
391
+ () =>
392
+ failpoint.hit("maintenance:mutation:armed").pipe(
393
+ Effect.andThen(body),
394
+ Effect.tap(() => failpoint.hit("maintenance:mutation:finished")),
395
+ ),
396
+ () => endMutation,
397
+ );
411
398
 
412
- if (!initialized) {
413
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
414
- }
415
- if (state.dirty > state.processed) {
416
- await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
417
- }
418
- }),
419
- );
420
- yield* failpoint.hit("maintenance:ensure:after");
421
- });
399
+ const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
400
+ yield* failpoint.hit("maintenance:ensure:before");
401
+ const now = yield* Clock.currentTimeMillis;
402
+
403
+ yield* runTransaction("ensure maintenance alarm", () =>
404
+ ctx.storage.transaction(async (transaction) => {
405
+ const { state, initialized } = await readMaintenanceState(transaction);
406
+
407
+ if (!initialized) {
408
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
409
+ }
410
+ if (state.dirty > state.processed) {
411
+ await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
412
+ }
413
+ }),
414
+ );
415
+ yield* failpoint.hit("maintenance:ensure:after");
416
+ });
417
+
418
+ const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
419
+ yield* failpoint.hit("maintenance:begin:before");
420
+ const now = yield* Clock.currentTimeMillis;
421
+
422
+ const result = yield* runTransaction("begin maintenance pass", () =>
423
+ ctx.storage.transaction(async (transaction) => {
424
+ const { state, initialized } = await readMaintenanceState(transaction);
425
+
426
+ if (!initialized) {
427
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
428
+ }
429
+ if (state.processed >= state.dirty) {
430
+ await transaction.deleteAlarm();
431
+
432
+ return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
433
+ }
434
+ // Pre-arm the earliest retry before recovery. A successful finish may move this slot
435
+ // LATER to its bounded backoff, which does not cancel the running handler.
436
+ await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
437
+
438
+ return { _tag: "Actionable" as const, generation: state.dirty };
439
+ }),
440
+ );
422
441
 
423
- const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
424
- yield* failpoint.hit("maintenance:begin:before");
425
- const now = yield* Clock.currentTimeMillis;
442
+ yield* failpoint.hit("maintenance:begin:after");
426
443
 
427
- const result = yield* runTransaction("begin maintenance pass", () =>
428
- ctx.storage.transaction(async (transaction) => {
429
- const { state, initialized } = await readMaintenanceState(transaction);
444
+ return result;
445
+ });
430
446
 
431
- if (!initialized) {
432
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
433
- }
434
- if (state.processed >= state.dirty) {
435
- await transaction.deleteAlarm();
447
+ const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed: boolean) {
448
+ const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
449
+ progressed ? 0 : count + 1,
450
+ );
436
451
 
437
- return { _tag: "CaughtUp" as const, nonterminal: state.nonterminal };
438
- }
439
- // Pre-arm the earliest retry before recovery. A successful finish may move this slot
440
- // LATER to its bounded backoff, which does not cancel the running handler.
441
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
452
+ if (progressed) return config.alarmBackoffBase;
453
+ const exponent = Math.min(priorStalls, 30);
454
+ const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
455
+ const jitter = yield* Random.next;
456
+ // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
457
+ // waiting longer than the deterministic bound.
458
+ const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
459
+
460
+ return Math.min(jittered, config.wakeScanInterval);
461
+ });
462
+
463
+ const pass = Effect.fn("ThreadMaintenance.pass")(function* (): Effect.fn.Return<
464
+ MaintenancePassReport,
465
+ MaintenancePassFailure
466
+ > {
467
+ const annotate = (report: MaintenancePassReport) =>
468
+ Effect.annotateCurrentSpan({
469
+ phase: report.phase,
470
+ recovered: report.recovered,
471
+ settled: report.settled,
472
+ nonterminal: report.nonterminal,
473
+ alarm: report.alarm,
474
+ }).pipe(Effect.as(report));
475
+
476
+ const started = yield* generationGate.withPermit(
477
+ Effect.gen(function* () {
478
+ const activeAtStart = yield* Ref.get(activeMutations);
479
+ const generation = yield* beginPass();
480
+
481
+ return { ...generation, activeAtStart };
482
+ }),
483
+ );
442
484
 
443
- return { _tag: "Actionable" as const, generation: state.dirty };
485
+ if (started._tag === "CaughtUp") {
486
+ return yield* annotate(
487
+ MaintenancePassReport.make({
488
+ phase: "caught-up",
489
+ recovered: 0,
490
+ settled: 0,
491
+ nonterminal: started.nonterminal,
492
+ alarm: "cleared",
444
493
  }),
445
494
  );
446
-
447
- yield* failpoint.hit("maintenance:begin:after");
448
-
449
- return result;
495
+ }
496
+ // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
497
+ const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
498
+ // Step 3 — one bounded drain pass over this Object's own lane.
499
+ const settlements = yield* runtime.processThreadResolved(identity.threadId);
500
+ // Observe residual state before acknowledging this exact pass-start generation.
501
+ const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
502
+ const reports = new Map(recovered.map((report) => [report.submissionId, report]));
503
+ const head = remaining[0];
504
+ const headWaiting = head !== undefined && stableExternalWait(head, reports);
505
+
506
+ const autonomous = remaining.some((snapshot, index) => {
507
+ // FIFO followers cannot execute through a stable external wait. Only plain queued
508
+ // input is dormant here; admission repairs and accepted aborts still need a pass.
509
+ if (
510
+ index > 0 &&
511
+ headWaiting &&
512
+ snapshot.state === "ready" &&
513
+ reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
514
+ )
515
+ return false;
516
+
517
+ return !stableExternalWait(snapshot, reports);
450
518
  });
451
519
 
452
- const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (
453
- progressed: boolean,
454
- ) {
455
- const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
456
- progressed ? 0 : count + 1,
457
- );
520
+ const progressed =
521
+ settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
458
522
 
459
- if (progressed) return config.alarmBackoffBase;
460
- const exponent = Math.min(priorStalls, 30);
461
- const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
462
- const jitter = yield* Random.next;
463
- // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
464
- // waiting longer than the deterministic bound.
465
- const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
523
+ const delay = autonomous ? yield* rearmDelay(progressed) : 0;
524
+ const now = yield* Clock.currentTimeMillis;
466
525
 
467
- return Math.min(jittered, config.wakeScanInterval);
468
- });
526
+ yield* failpoint.hit("maintenance:finish:before");
469
527
 
470
- const pass = Effect.fn("ThreadMaintenance.pass")(function* (): Effect.fn.Return<
471
- MaintenancePassReport,
472
- MaintenancePassFailure
473
- > {
474
- const annotate = (report: MaintenancePassReport) =>
475
- Effect.annotateCurrentSpan({
476
- phase: report.phase,
477
- recovered: report.recovered,
478
- settled: report.settled,
479
- nonterminal: report.nonterminal,
480
- alarm: report.alarm,
481
- }).pipe(Effect.as(report));
482
-
483
- const started = yield* generationGate.withPermit(
484
- Effect.gen(function* () {
485
- const activeAtStart = yield* Ref.get(activeMutations);
486
- const generation = yield* beginPass();
487
-
488
- return { ...generation, activeAtStart };
489
- }),
490
- );
528
+ const alarmDisposition = yield* generationGate.withPermit(
529
+ Effect.gen(function* () {
530
+ const active = yield* Ref.get(activeMutations);
491
531
 
492
- if (started._tag === "CaughtUp") {
493
- return yield* annotate(
494
- MaintenancePassReport.make({
495
- phase: "caught-up",
496
- recovered: 0,
497
- settled: 0,
498
- nonterminal: started.nonterminal,
499
- alarm: "cleared",
500
- }),
501
- );
502
- }
503
- // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
504
- const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
505
- // Step 3 — one bounded drain pass over this Object's own lane.
506
- const settlements = yield* runtime.processThreadResolved(identity.threadId, bindings);
507
- // Observe residual state before acknowledging this exact pass-start generation.
508
- const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
509
- const reports = new Map(recovered.map((report) => [report.submissionId, report]));
510
- const head = remaining[0];
511
- const headWaiting = head !== undefined && stableExternalWait(head, reports);
512
-
513
- const autonomous = remaining.some((snapshot, index) => {
514
- // FIFO followers cannot execute through a stable external wait. Only plain queued
515
- // input is dormant here; admission repairs and accepted aborts still need a pass.
516
- if (
517
- index > 0 &&
518
- headWaiting &&
519
- snapshot.state === "ready" &&
520
- reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput"
521
- )
522
- return false;
523
-
524
- return !stableExternalWait(snapshot, reports);
525
- });
526
-
527
- const progressed =
528
- settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
529
-
530
- const delay = autonomous ? yield* rearmDelay(progressed) : 0;
531
- const now = yield* Clock.currentTimeMillis;
532
-
533
- yield* failpoint.hit("maintenance:finish:before");
532
+ return yield* runTransaction("finish maintenance pass", () =>
533
+ ctx.storage.transaction(async (transaction) => {
534
+ const { state } = await readMaintenanceState(transaction);
534
535
 
535
- const alarmDisposition = yield* generationGate.withPermit(
536
- Effect.gen(function* () {
537
- const active = yield* Ref.get(activeMutations);
538
-
539
- return yield* runTransaction("finish maintenance pass", () =>
540
- ctx.storage.transaction(async (transaction) => {
541
- const { state } = await readMaintenanceState(transaction);
542
-
543
- // Autonomous work and in-flight mutations intentionally leave the observed
544
- // generation dirty. Otherwise acknowledge only the pass-start generation.
545
- const processed =
546
- autonomous || started.activeAtStart > 0 || active > 0
536
+ // Autonomous work and in-flight mutations intentionally leave the observed
537
+ // generation dirty. Otherwise acknowledge only the pass-start generation.
538
+ const processed =
539
+ autonomous || started.activeAtStart > 0 || active > 0
540
+ ? state.processed
541
+ : state.processed > started.generation
547
542
  ? state.processed
548
- : state.processed > started.generation
549
- ? state.processed
550
- : started.generation;
551
-
552
- const next = ThreadMaintenanceState.make({
553
- ...state,
554
- processed,
555
- nonterminal: remaining.length,
556
- });
557
-
558
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
559
- if (autonomous) {
560
- // Replace the crash-fallback slot with this pass's bounded backoff. The target
561
- // is never earlier than the begin-pass fallback, so workerd does not cancel
562
- // this running alarm handler before its report/span can complete.
563
- await transaction.setAlarm(now + delay);
564
-
565
- return "rearmed" as const;
566
- }
567
- if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
568
- // A mutation overlapped this pass's observation window or raced
569
- // acknowledgement. It stays dirty and its pre-armed bounded alarm survives;
570
- // unseen effects are never acknowledged. Do not accelerate that future alarm
571
- // from inside the current handler: workerd cancels a running handler when it
572
- // writes an earlier slot.
573
- await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
574
-
575
- return "rearmed" as const;
576
- }
577
- await transaction.deleteAlarm();
578
-
579
- return "cleared" as const;
580
- }),
581
- );
582
- }),
583
- );
584
-
585
- yield* failpoint.hit("maintenance:finish:after");
586
- if (alarmDisposition === "cleared") {
587
- yield* Ref.set(stalls, 0);
588
- }
543
+ : started.generation;
544
+
545
+ const next = ThreadMaintenanceState.make({
546
+ ...state,
547
+ processed,
548
+ nonterminal: remaining.length,
549
+ });
550
+
551
+ await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
552
+ if (autonomous) {
553
+ // Replace the crash-fallback slot with this pass's bounded backoff. The target
554
+ // is never earlier than the begin-pass fallback, so workerd does not cancel
555
+ // this running alarm handler before its report/span can complete.
556
+ await transaction.setAlarm(now + delay);
557
+
558
+ return "rearmed" as const;
559
+ }
560
+ if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
561
+ // A mutation overlapped this pass's observation window or raced
562
+ // acknowledgement. It stays dirty and its pre-armed bounded alarm survives;
563
+ // unseen effects are never acknowledged. Do not accelerate that future alarm
564
+ // from inside the current handler: workerd cancels a running handler when it
565
+ // writes an earlier slot.
566
+ await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
567
+
568
+ return "rearmed" as const;
569
+ }
570
+ await transaction.deleteAlarm();
589
571
 
590
- return yield* annotate(
591
- MaintenancePassReport.make({
592
- phase: "actionable",
593
- recovered: recovered.length,
594
- settled: settlements.length,
595
- nonterminal: remaining.length,
596
- alarm: alarmDisposition,
597
- }),
598
- );
599
- });
572
+ return "cleared" as const;
573
+ }),
574
+ );
575
+ }),
576
+ );
600
577
 
601
- return ThreadMaintenance.of({
602
- // A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.
603
- pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
604
- ensureAlarm: ensureAlarm(),
605
- withMutation,
606
- });
607
- }),
608
- );
578
+ yield* failpoint.hit("maintenance:finish:after");
579
+ if (alarmDisposition === "cleared") {
580
+ yield* Ref.set(stalls, 0);
581
+ }
582
+
583
+ return yield* annotate(
584
+ MaintenancePassReport.make({
585
+ phase: "actionable",
586
+ recovered: recovered.length,
587
+ settled: settlements.length,
588
+ nonterminal: remaining.length,
589
+ alarm: alarmDisposition,
590
+ }),
591
+ );
592
+ });
593
+
594
+ return ThreadMaintenance.of({
595
+ // A mid-pass immediate hint is droppable; durable dirty state decides the final alarm.
596
+ pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
597
+ ensureAlarm: ensureAlarm(),
598
+ withMutation,
599
+ });
600
+ }),
601
+ );
609
602
  }
@@ -14,20 +14,20 @@ import {
14
14
  PageScrapeCaptured,
15
15
  PageStructuredCaptured,
16
16
  PageCaptureUnsupportedError,
17
- SandboxImplementation,
18
17
  type PageCaptureAction,
19
18
  type PageCaptureCapture,
20
19
  type PageCaptureError,
21
20
  type PageCaptureOutput,
22
21
  type PageCaptureRequest,
23
- } from "@effect-agent/sandbox";
22
+ } from "@effect-agent/sandbox/PageCapture";
23
+ import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
24
24
  import { Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
25
25
  import { HttpClient, HttpClientRequest, type HttpClientError } from "effect/unstable/http";
26
26
 
27
27
  import {
28
28
  BrowserQuickActionWorkersAi,
29
29
  type BrowserQuickActionWorkersAiPolicy,
30
- } from "./browser-quick-action.ts";
30
+ } from "./CloudflareBrowser.ts";
31
31
 
32
32
  /** Node-safe REST PageCapture implementation; it never imports Worker runtime modules. */
33
33
  export const browserRestCaptureImplementation = SandboxImplementation.make({
@@ -5,11 +5,11 @@ import {
5
5
  PageCrawlRateLimitedError,
6
6
  PageCrawlRecord,
7
7
  PageCrawlTerminalError,
8
- SandboxImplementation,
9
8
  type PageCrawlCrawl,
10
9
  type PageCrawlError,
11
10
  type PageCrawlRequest,
12
- } from "@effect-agent/sandbox";
11
+ } from "@effect-agent/sandbox/PageCrawl";
12
+ import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
13
13
  import {
14
14
  Duration,
15
15
  Effect,
@@ -1,5 +1,5 @@
1
- import type { ThreadId } from "@effect-agent/core";
2
- import type { ProducerId } from "@effect-agent/thread";
1
+ import { type ThreadId } from "@effect-agent/core/Identifiers";
2
+ import { type ProducerId } from "@effect-agent/thread/Records";
3
3
  import { Context, Effect, Layer, Predicate, Schema } from "effect";
4
4
 
5
5
  /**