@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.
- package/dist/Alarm.d.mts +119 -0
- package/dist/Alarm.mjs +317 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/{browser-rest-capture.d.mts → BrowserRestCapture.d.mts} +5 -4
- package/dist/{browser-rest-capture.mjs → BrowserRestCapture.mjs} +5 -4
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/{browser-rest-crawl.d.mts → BrowserRestCrawl.d.mts} +4 -3
- package/dist/{browser-rest-crawl.mjs → BrowserRestCrawl.mjs} +4 -3
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/{bindings-BrVEOkfe.d.mts → CloudflareBindings.d.mts} +7 -5
- package/dist/CloudflareBindings.mjs +98 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/{browser-quick-action.mjs → CloudflareBrowser-1jeZx3_g.mjs} +19 -4
- package/dist/CloudflareBrowser-1jeZx3_g.mjs.map +1 -0
- package/dist/{browser-quick-action.d.mts → CloudflareBrowser-DJq53niJ.d.mts} +9 -18
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +618 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +140 -0
- package/dist/CloudflareMemory.mjs +168 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/{scheduling.d.mts → CloudflareScheduling.d.mts} +9 -6
- package/dist/{scheduling.mjs → CloudflareScheduling.mjs} +22 -8
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/{subscriptions.d.mts → CloudflareSubscriptions.d.mts} +11 -6
- package/dist/{subscriptions.mjs → CloudflareSubscriptions.mjs} +26 -7
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +715 -0
- package/dist/CloudflareThreadClient.mjs +364 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/{interactive-browser.d.mts → InteractiveBrowser.d.mts} +5 -4
- package/dist/{interactive-browser.mjs → InteractiveBrowser.mjs} +6 -4
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/{protected-browser.d.mts → ProtectedBrowser.d.mts} +4 -3
- package/dist/{protected-browser.mjs → ProtectedBrowser.mjs} +4 -3
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-Ia94R_B1.d.mts +735 -0
- package/dist/ThreadObject-IrGuIO4a.mjs +575 -0
- package/dist/ThreadObject-IrGuIO4a.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +60 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/{browser-session-lifecycle-ZGgb3pnK.mjs → browser-session-lifecycle-CUbVfEgP.mjs} +2 -2
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/{browser-session-lifecycle-DqntvG-Y.d.mts → browser-session-lifecycle-DJpqLXy_.d.mts} +2 -2
- package/dist/index.d.mts +12 -1833
- package/dist/index.mjs +12 -1357
- package/dist/prepared-admission-DRBhl2Sp.mjs +63 -0
- package/dist/prepared-admission-DRBhl2Sp.mjs.map +1 -0
- package/package.json +1 -1
- package/src/{alarm.ts → Alarm.ts} +265 -272
- package/src/{browser-rest-capture.ts → BrowserRestCapture.ts} +3 -3
- package/src/{browser-rest-crawl.ts → BrowserRestCrawl.ts} +2 -2
- package/src/{bindings.ts → CloudflareBindings.ts} +2 -2
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +3 -3
- package/src/{config.ts → CloudflareConfig.ts} +3 -2
- package/src/{memory.ts → CloudflareMemory.ts} +15 -12
- package/src/{scheduling.ts → CloudflareScheduling.ts} +12 -11
- package/src/{subscriptions.ts → CloudflareSubscriptions.ts} +14 -12
- package/src/{client.ts → CloudflareThreadClient.ts} +27 -20
- package/src/{interactive-browser.ts → InteractiveBrowser.ts} +9 -5
- package/src/{thread-object.ts → ThreadObject.ts} +37 -29
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +5 -5
- package/src/index.ts +11 -30
- package/src/{browser-quick-action.ts → internal/browser-quick-action.ts} +10 -8
- package/src/{layers.ts → internal/layers.ts} +42 -33
- package/src/{prepared-admission.ts → internal/prepared-admission.ts} +7 -7
- package/src/{transport.ts → internal/transport.ts} +5 -2
- package/src/protected-browser/binding.ts +3 -2
- package/src/protected-browser/native.ts +2 -2
- package/src/protected-browser/policy.ts +2 -2
- package/dist/browser-quick-action.mjs.map +0 -1
- package/dist/browser-rest-capture.mjs.map +0 -1
- package/dist/browser-rest-crawl.mjs.map +0 -1
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/interactive-browser.mjs.map +0 -1
- package/dist/prepared-admission-BKp_Upw2.mjs +0 -890
- package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
- package/dist/protected-browser.mjs.map +0 -1
- package/dist/scheduling.mjs.map +0 -1
- package/dist/subscriptions.mjs.map +0 -1
- /package/src/{protected-browser.ts → ProtectedBrowser.ts} +0 -0
- /package/src/{boundary.ts → internal/boundary.ts} +0 -0
- /package/src/{browser-session-lifecycle.ts → internal/browser-session-lifecycle.ts} +0 -0
- /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
|
-
|
|
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 "./
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
367
|
+
const next = ThreadMaintenanceState.make({
|
|
368
|
+
...state,
|
|
369
|
+
dirty: state.dirty + 1n,
|
|
370
|
+
});
|
|
407
371
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
424
|
-
yield* failpoint.hit("maintenance:begin:before");
|
|
425
|
-
const now = yield* Clock.currentTimeMillis;
|
|
442
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
426
443
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const { state, initialized } = await readMaintenanceState(transaction);
|
|
444
|
+
return result;
|
|
445
|
+
});
|
|
430
446
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
453
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
468
|
-
});
|
|
526
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
469
527
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
nonterminal: remaining.length,
|
|
596
|
-
alarm: alarmDisposition,
|
|
597
|
-
}),
|
|
598
|
-
);
|
|
599
|
-
});
|
|
572
|
+
return "cleared" as const;
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
600
577
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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 "./
|
|
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
|
|
2
|
-
import type
|
|
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
|
/**
|