@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.15
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/index.d.mts +107 -104
- package/dist/index.mjs +208 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/alarm.ts +330 -113
- package/src/conversation-object.ts +34 -35
- package/src/index.ts +3 -2
- package/src/layers.ts +21 -5
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Semaphore, Stream } from "effect";
|
|
2
2
|
import { AbortCommand, AbortIntent, AdmissionConflict, AgentBindingResolver, AppendConflict, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, CanonicalRecordEnvelope, CanonicalSequence, ConversationNotMaterialized, ConversationRead, ConversationStore, ConversationStoreError, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeploymentId, DigestError, DurableAgentRuntime, DurableRuntimeConfig, DurableRuntimeFailpoint, DurableRuntimeFailpointError, FenceRejected, IdempotencyKey, IntegrityReport, JoinedToHost, LedgerError, ObligationReport, ObligationThresholds, OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, OwnershipLost, PersistedJson, Principal, ProducerId, Receipt, RecoveryExplanation, RecoveryReport, RetryCommand, RetryRefused, RunJournalError, Settlement, SettlementConflict, SubmissionLedger, SubmissionLookupByKey, ToolReconciler, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, WakeScheduler } from "@effect-agent/session";
|
|
3
3
|
import { ConversationPortTransport, DEFAULT_MAX_STORED_VALUE_BYTES, conversationStoreLayer, handleEncodedPortRequest, portTransportFailure, routedConversationStoreLayer, routedSubmissionLedgerLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer } from "@effect-agent/storage-cloudflare";
|
|
4
4
|
import { AgentId, AgentInputError, ConversationId, SubmissionId } from "@effect-agent/core";
|
|
@@ -180,11 +180,10 @@ const CLOUDFLARE_RUNTIME_DEFAULTS = {
|
|
|
180
180
|
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
181
181
|
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
182
182
|
*
|
|
183
|
-
* The alarm invariant (plan §1.4): committed
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* alarms are harmless by design: a pass over an all-settled lane simply deletes the slot.
|
|
183
|
+
* The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
|
|
184
|
+
* maintenance generation and a committed alarm. Stable externally-driven waits may be
|
|
185
|
+
* nonterminal without retaining an alarm; their resolving mutation advances the generation and
|
|
186
|
+
* restores the alarm atomically.
|
|
188
187
|
*/
|
|
189
188
|
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
190
189
|
var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
|
|
@@ -207,7 +206,6 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
|
|
|
207
206
|
* on top of the already-committed pre-armed alarm.
|
|
208
207
|
*/
|
|
209
208
|
const runningPasses = yield* Ref.make(0);
|
|
210
|
-
const wakeDeferred = yield* Ref.make(false);
|
|
211
209
|
const scheduled = Effect.tryPromise({
|
|
212
210
|
try: () => ctx.storage.getAlarm(),
|
|
213
211
|
catch: alarmFailure("get alarm")
|
|
@@ -218,14 +216,8 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
|
|
|
218
216
|
});
|
|
219
217
|
const ensureScheduledBy = (epochMillis) => scheduled.pipe(Effect.flatMap((existing) => Option.isSome(existing) && existing.value <= epochMillis ? Effect.void : scheduleAt(epochMillis)));
|
|
220
218
|
const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
|
|
221
|
-
const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ?
|
|
222
|
-
|
|
223
|
-
* Flush after the LAST concurrent pass: the re-arm lands at the very end of the alarm
|
|
224
|
-
* handler, where a superseding cancellation only re-delivers to the already-idempotent
|
|
225
|
-
* pass. Runs inside `Effect.ensuring`, so flush failures are logged, never raised.
|
|
226
|
-
*/
|
|
227
|
-
const flushDeferredWake = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : Ref.getAndSet(wakeDeferred, false).pipe(Effect.flatMap((wanted) => wanted ? armNow : Effect.void))), Effect.catch((error) => Effect.logWarning("DurableAlarmService: deferred wake flush failed", error)));
|
|
228
|
-
const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1).pipe(Effect.andThen(flushDeferredWake))));
|
|
219
|
+
const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
|
|
220
|
+
const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
|
|
229
221
|
const cancel = Effect.tryPromise({
|
|
230
222
|
try: () => ctx.storage.deleteAlarm(),
|
|
231
223
|
catch: alarmFailure("delete alarm")
|
|
@@ -242,35 +234,78 @@ var DurableAlarmService = class DurableAlarmService extends Context.Service()("@
|
|
|
242
234
|
};
|
|
243
235
|
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
244
236
|
var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
|
|
237
|
+
/** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */
|
|
238
|
+
phase: Schema.Literals(["caught-up", "actionable"]),
|
|
245
239
|
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
246
240
|
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
247
241
|
/** Settlements the drain pass finalized. */
|
|
248
242
|
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
249
243
|
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
250
244
|
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
251
|
-
/** `rearmed`
|
|
245
|
+
/** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
|
|
252
246
|
alarm: Schema.Literals(["rearmed", "cleared"])
|
|
253
247
|
}) {};
|
|
248
|
+
/** Test-only fault authority; production uses the inert layer. */
|
|
249
|
+
var ConversationMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ConversationMaintenanceFailpoint") {
|
|
250
|
+
static layer = Layer.succeed(this)({ hit: () => Effect.void });
|
|
251
|
+
};
|
|
252
|
+
const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
|
|
253
|
+
/** Versioned, platform-private maintenance state stored through Durable Object KV. */
|
|
254
|
+
var ConversationMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ConversationMaintenanceState")({
|
|
255
|
+
schemaVersion: Schema.Literal(1),
|
|
256
|
+
dirty: MaintenanceGeneration,
|
|
257
|
+
processed: MaintenanceGeneration,
|
|
258
|
+
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
259
|
+
}) {};
|
|
260
|
+
const MAINTENANCE_STATE_KEY = "effect-agent:conversation-maintenance:v1";
|
|
261
|
+
const decodeMaintenanceState = Schema.decodeUnknownSync(ConversationMaintenanceState);
|
|
262
|
+
const encodeMaintenanceState = Schema.encodeSync(ConversationMaintenanceState);
|
|
263
|
+
const initialMaintenanceState = () => ConversationMaintenanceState.make({
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
dirty: 1n,
|
|
266
|
+
processed: 0n,
|
|
267
|
+
nonterminal: 0
|
|
268
|
+
});
|
|
269
|
+
const readMaintenanceState = async (transaction) => {
|
|
270
|
+
const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
|
|
271
|
+
return encoded === void 0 ? {
|
|
272
|
+
state: initialMaintenanceState(),
|
|
273
|
+
initialized: false
|
|
274
|
+
} : {
|
|
275
|
+
state: decodeMaintenanceState(encoded),
|
|
276
|
+
initialized: true
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
const ensureTransactionAlarmBy = async (transaction, deadline) => {
|
|
280
|
+
const scheduled = await transaction.getAlarm();
|
|
281
|
+
if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
|
|
282
|
+
};
|
|
283
|
+
const stableExternalWait = (snapshot, reports) => {
|
|
284
|
+
switch (snapshot.state) {
|
|
285
|
+
case "suspended":
|
|
286
|
+
case "unknown":
|
|
287
|
+
case "joined": return true;
|
|
288
|
+
case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
|
|
289
|
+
case "input-applied":
|
|
290
|
+
case "joining":
|
|
291
|
+
case "ready":
|
|
292
|
+
case "running":
|
|
293
|
+
case "settled":
|
|
294
|
+
case "terminalizing": return false;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
254
297
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* `pass` = pre-arm → `runRecovery` → `processConversationResolved` → re-arm-or-clear:
|
|
298
|
+
* Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
|
|
258
299
|
*
|
|
259
|
-
*
|
|
260
|
-
* eviction (or thrown failure → workerd alarm retry) at any point of the pass leaves a
|
|
261
|
-
* committed alarm and the fresh incarnation converges without an incoming request.
|
|
262
|
-
* 2. **Reconcile before new work** (exit gate): every pass classifies and repairs every
|
|
263
|
-
* nonterminal Submission before the drain claims anything; the pure classifier and the
|
|
264
|
-
* repair executors are idempotent, so at-least-once alarm delivery re-runs them safely.
|
|
265
|
-
* 3. **Drain**: one bounded `processConversationResolved` pass over this Object's lane — the
|
|
266
|
-
* D-P6-1 shape; no infinite `runResolvedWorker` loop ever pins the Object.
|
|
267
|
-
* 4. **Re-arm policy**: nonterminal work re-arms at `now + min(backoff-with-jitter,
|
|
268
|
-
* wakeScanInterval)` — the scan interval bounds every wait (lease expiry of a dead
|
|
269
|
-
* incarnation included, since claims retry each pass) and the backoff (reset on progress,
|
|
270
|
-
* grown otherwise) keeps stuck lanes from busy-spinning; all settled clears the slot.
|
|
300
|
+
* `pass` = generation snapshot/pre-arm → recovery → bounded drain → generation acknowledgement:
|
|
271
301
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
302
|
+
* 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
|
|
303
|
+
* alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
|
|
304
|
+
* 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.
|
|
305
|
+
* 3. The final transaction acknowledges only the generation observed at pass start. A racing
|
|
306
|
+
* mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
|
|
307
|
+
* 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
|
|
308
|
+
* recovery states leave their generation dirty and retain bounded backoff rearming.
|
|
274
309
|
*/
|
|
275
310
|
var ConversationMaintenance = class ConversationMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ConversationMaintenance") {
|
|
276
311
|
static layer = Layer.effect(ConversationMaintenance)(Effect.gen(function* () {
|
|
@@ -280,13 +315,76 @@ var ConversationMaintenance = class ConversationMaintenance extends Context.Serv
|
|
|
280
315
|
const alarm = yield* DurableAlarmService;
|
|
281
316
|
const config = yield* CloudflareDurableRuntimeConfig;
|
|
282
317
|
const identity = yield* ConversationObjectIdentity;
|
|
318
|
+
const { ctx } = yield* DurableObjectContext;
|
|
319
|
+
const failpoint = yield* ConversationMaintenanceFailpoint;
|
|
283
320
|
/**
|
|
284
321
|
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
285
322
|
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
286
323
|
*/
|
|
287
324
|
const stalls = yield* Ref.make(0);
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Incarnation-local mutation count guarded with the generation transactions below. It is
|
|
327
|
+
* deliberately not durable: after eviction every begun mutation has stopped, while its
|
|
328
|
+
* pre-armed dirty generation remains durable for recovery. The short gate never spans the
|
|
329
|
+
* caller's mutation or cross-Object I/O.
|
|
330
|
+
*/
|
|
331
|
+
const activeMutations = yield* Ref.make(0);
|
|
332
|
+
const generationGate = yield* Semaphore.make(1);
|
|
333
|
+
const maintenancePassGate = yield* Semaphore.make(1);
|
|
334
|
+
const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
|
|
335
|
+
const runTransaction = (operation, transaction) => Effect.tryPromise({
|
|
336
|
+
try: transaction,
|
|
337
|
+
catch: alarmFailure(operation)
|
|
338
|
+
});
|
|
339
|
+
const beginMutation = Effect.fn("ConversationMaintenance.beginMutation")(function* () {
|
|
340
|
+
yield* failpoint.hit("maintenance:dirty:before");
|
|
341
|
+
const now = yield* Clock.currentTimeMillis;
|
|
342
|
+
yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
|
|
343
|
+
const { state } = await readMaintenanceState(transaction);
|
|
344
|
+
const next = ConversationMaintenanceState.make({
|
|
345
|
+
...state,
|
|
346
|
+
dirty: state.dirty + 1n
|
|
347
|
+
});
|
|
348
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
349
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
350
|
+
}));
|
|
351
|
+
yield* failpoint.hit("maintenance:dirty:after");
|
|
352
|
+
yield* Ref.update(activeMutations, (active) => active + 1);
|
|
353
|
+
});
|
|
354
|
+
const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
|
|
355
|
+
const withMutation = (body) => Effect.acquireUseRelease(generationGate.withPermit(beginMutation()), () => failpoint.hit("maintenance:mutation:armed").pipe(Effect.andThen(body), Effect.tap(() => failpoint.hit("maintenance:mutation:finished"))), () => endMutation);
|
|
356
|
+
const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
|
|
357
|
+
yield* failpoint.hit("maintenance:ensure:before");
|
|
358
|
+
const now = yield* Clock.currentTimeMillis;
|
|
359
|
+
yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
|
|
360
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
361
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
362
|
+
if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
363
|
+
}));
|
|
364
|
+
yield* failpoint.hit("maintenance:ensure:after");
|
|
365
|
+
});
|
|
366
|
+
const beginPass = Effect.fn("ConversationMaintenance.beginPass")(function* () {
|
|
367
|
+
yield* failpoint.hit("maintenance:begin:before");
|
|
368
|
+
const now = yield* Clock.currentTimeMillis;
|
|
369
|
+
const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
370
|
+
const { state, initialized } = await readMaintenanceState(transaction);
|
|
371
|
+
if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
|
|
372
|
+
if (state.processed >= state.dirty) {
|
|
373
|
+
await transaction.deleteAlarm();
|
|
374
|
+
return {
|
|
375
|
+
_tag: "CaughtUp",
|
|
376
|
+
nonterminal: state.nonterminal
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
|
|
380
|
+
return {
|
|
381
|
+
_tag: "Actionable",
|
|
382
|
+
generation: state.dirty
|
|
383
|
+
};
|
|
384
|
+
}));
|
|
385
|
+
yield* failpoint.hit("maintenance:begin:after");
|
|
386
|
+
return result;
|
|
387
|
+
});
|
|
290
388
|
const rearmDelay = Effect.fn("ConversationMaintenance.rearmDelay")(function* (progressed) {
|
|
291
389
|
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
|
|
292
390
|
if (progressed) return config.alarmBackoffBase;
|
|
@@ -297,49 +395,73 @@ var ConversationMaintenance = class ConversationMaintenance extends Context.Serv
|
|
|
297
395
|
return Math.min(jittered, config.wakeScanInterval);
|
|
298
396
|
});
|
|
299
397
|
const pass = Effect.fn("ConversationMaintenance.pass")(function* () {
|
|
300
|
-
|
|
398
|
+
const annotate = (report) => Effect.annotateCurrentSpan({
|
|
399
|
+
phase: report.phase,
|
|
400
|
+
recovered: report.recovered,
|
|
401
|
+
settled: report.settled,
|
|
402
|
+
nonterminal: report.nonterminal,
|
|
403
|
+
alarm: report.alarm
|
|
404
|
+
}).pipe(Effect.as(report));
|
|
405
|
+
const started = yield* generationGate.withPermit(Effect.gen(function* () {
|
|
406
|
+
const activeAtStart = yield* Ref.get(activeMutations);
|
|
407
|
+
return {
|
|
408
|
+
...yield* beginPass(),
|
|
409
|
+
activeAtStart
|
|
410
|
+
};
|
|
411
|
+
}));
|
|
412
|
+
if (started._tag === "CaughtUp") return yield* annotate(MaintenancePassReport.make({
|
|
413
|
+
phase: "caught-up",
|
|
414
|
+
recovered: 0,
|
|
415
|
+
settled: 0,
|
|
416
|
+
nonterminal: started.nonterminal,
|
|
417
|
+
alarm: "cleared"
|
|
418
|
+
}));
|
|
301
419
|
const recovered = yield* runtime.runRecovery;
|
|
302
420
|
const settlements = yield* runtime.processConversationResolved(identity.conversationId).pipe(Effect.provideService(AgentBindingResolver, resolver));
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
yield* Ref.set(stalls, 0);
|
|
307
|
-
const appeared = yield* countNonterminal;
|
|
308
|
-
if (appeared > 0) {
|
|
309
|
-
yield* preArm;
|
|
310
|
-
return MaintenancePassReport.make({
|
|
311
|
-
recovered: recovered.length,
|
|
312
|
-
settled: settlements.length,
|
|
313
|
-
nonterminal: appeared,
|
|
314
|
-
alarm: "rearmed"
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
return MaintenancePassReport.make({
|
|
318
|
-
recovered: recovered.length,
|
|
319
|
-
settled: settlements.length,
|
|
320
|
-
nonterminal,
|
|
321
|
-
alarm: "cleared"
|
|
322
|
-
});
|
|
323
|
-
}
|
|
421
|
+
const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
422
|
+
const reports = new Map(recovered.map((report) => [report.submissionId, report]));
|
|
423
|
+
const autonomous = remaining.some((snapshot) => !stableExternalWait(snapshot, reports));
|
|
324
424
|
const progressed = settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
|
|
325
|
-
const delay = yield* rearmDelay(progressed);
|
|
425
|
+
const delay = autonomous ? yield* rearmDelay(progressed) : 0;
|
|
326
426
|
const now = yield* Clock.currentTimeMillis;
|
|
327
|
-
yield*
|
|
328
|
-
|
|
427
|
+
yield* failpoint.hit("maintenance:finish:before");
|
|
428
|
+
const alarmDisposition = yield* generationGate.withPermit(Effect.gen(function* () {
|
|
429
|
+
const active = yield* Ref.get(activeMutations);
|
|
430
|
+
return yield* runTransaction("finish maintenance pass", () => ctx.storage.transaction(async (transaction) => {
|
|
431
|
+
const { state } = await readMaintenanceState(transaction);
|
|
432
|
+
const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
|
|
433
|
+
const next = ConversationMaintenanceState.make({
|
|
434
|
+
...state,
|
|
435
|
+
processed,
|
|
436
|
+
nonterminal: remaining.length
|
|
437
|
+
});
|
|
438
|
+
await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
|
|
439
|
+
if (autonomous) {
|
|
440
|
+
await transaction.setAlarm(now + delay);
|
|
441
|
+
return "rearmed";
|
|
442
|
+
}
|
|
443
|
+
if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
|
|
444
|
+
await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
|
|
445
|
+
return "rearmed";
|
|
446
|
+
}
|
|
447
|
+
await transaction.deleteAlarm();
|
|
448
|
+
return "cleared";
|
|
449
|
+
}));
|
|
450
|
+
}));
|
|
451
|
+
yield* failpoint.hit("maintenance:finish:after");
|
|
452
|
+
if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
|
|
453
|
+
return yield* annotate(MaintenancePassReport.make({
|
|
454
|
+
phase: "actionable",
|
|
329
455
|
recovered: recovered.length,
|
|
330
456
|
settled: settlements.length,
|
|
331
|
-
nonterminal,
|
|
332
|
-
alarm:
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
|
|
336
|
-
if ((yield* countNonterminal) === 0) return;
|
|
337
|
-
yield* preArm;
|
|
457
|
+
nonterminal: remaining.length,
|
|
458
|
+
alarm: alarmDisposition
|
|
459
|
+
}));
|
|
338
460
|
});
|
|
339
461
|
return ConversationMaintenance.of({
|
|
340
|
-
pass: alarm.withWakesDeferred(pass()),
|
|
462
|
+
pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
|
|
341
463
|
ensureAlarm: ensureAlarm(),
|
|
342
|
-
|
|
464
|
+
withMutation
|
|
343
465
|
});
|
|
344
466
|
}));
|
|
345
467
|
};
|
|
@@ -519,6 +641,7 @@ var CloudflareDurableRuntime = class {
|
|
|
519
641
|
abortPollInterval: Duration.millis(config.abortPollInterval)
|
|
520
642
|
}));
|
|
521
643
|
const runtimeFailpointLayer = options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
|
|
644
|
+
const maintenanceFailpointLayer = options.maintenanceFailpoint === void 0 ? ConversationMaintenanceFailpoint.layer : Layer.succeed(ConversationMaintenanceFailpoint)({ hit: options.maintenanceFailpoint(ctx) });
|
|
522
645
|
const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
|
|
523
646
|
const bindingResolverLayer = Layer.effect(AgentBindingResolver)(Effect.map(resolveBindings(options.bindings, {
|
|
524
647
|
ctx,
|
|
@@ -526,7 +649,7 @@ var CloudflareDurableRuntime = class {
|
|
|
526
649
|
conversationId,
|
|
527
650
|
producerId
|
|
528
651
|
}), (bindings) => AgentBindingResolver.fromBindings(bindings)));
|
|
529
|
-
const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer);
|
|
652
|
+
const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer, maintenanceFailpointLayer);
|
|
530
653
|
const runtimeStack = DurableAgentRuntime.layer.pipe(Layer.provideMerge(routedPorts), Layer.provideMerge(cloudflareWakeSchedulerLayer), Layer.provideMerge(runtimeConfigLayer), Layer.provideMerge(bindingResolverLayer), Layer.provide(Layer.mergeAll(runtimeFailpointLayer, reconcilerLayer, BrowserCrypto.layer)), Layer.provideMerge(base));
|
|
531
654
|
return Layer.mergeAll(runtimeStack, ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)), portsEndpointLayer);
|
|
532
655
|
}));
|
|
@@ -862,13 +985,12 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
|
|
|
862
985
|
const maintenance = yield* ConversationMaintenance;
|
|
863
986
|
const runtime = yield* DurableAgentRuntime;
|
|
864
987
|
yield* gateAdmissionLimits(request);
|
|
865
|
-
yield* maintenance.
|
|
866
|
-
const receipt = yield* runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
988
|
+
const receipt = yield* maintenance.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
867
989
|
conversationId: identity.conversationId,
|
|
868
990
|
principal: request.principal,
|
|
869
991
|
idempotencyKey: request.idempotencyKey,
|
|
870
992
|
definitions: request.definitions
|
|
871
|
-
});
|
|
993
|
+
}));
|
|
872
994
|
return SubmitSucceeded.make({ receipt });
|
|
873
995
|
})), respond, Effect.flatMap(encodeResponse));
|
|
874
996
|
const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
|
|
@@ -892,8 +1014,7 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
|
|
|
892
1014
|
const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
893
1015
|
const maintenance = yield* ConversationMaintenance;
|
|
894
1016
|
const runtime = yield* DurableAgentRuntime;
|
|
895
|
-
yield* maintenance.
|
|
896
|
-
const intent = yield* runtime.abort(command);
|
|
1017
|
+
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
897
1018
|
return AbortRecorded.make({ intent });
|
|
898
1019
|
})), respond, Effect.flatMap(encodeResponse));
|
|
899
1020
|
/**
|
|
@@ -907,15 +1028,13 @@ const deniedToProtocolFailure = (denied) => Effect.fail(HostProtocolError.make({
|
|
|
907
1028
|
const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
908
1029
|
const maintenance = yield* ConversationMaintenance;
|
|
909
1030
|
const runtime = yield* DurableAgentRuntime;
|
|
910
|
-
yield* maintenance.
|
|
911
|
-
const intent = yield* runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
1031
|
+
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)));
|
|
912
1032
|
return ApprovalRecorded.make({ intent });
|
|
913
1033
|
})), respond, Effect.flatMap(encodeResponse));
|
|
914
1034
|
const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
915
1035
|
const maintenance = yield* ConversationMaintenance;
|
|
916
1036
|
const runtime = yield* DurableAgentRuntime;
|
|
917
|
-
yield* maintenance.
|
|
918
|
-
const intent = yield* runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
1037
|
+
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)));
|
|
919
1038
|
return UnknownResolutionRecorded.make({ intent });
|
|
920
1039
|
})), respond, Effect.flatMap(encodeResponse));
|
|
921
1040
|
/** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
|
|
@@ -983,8 +1102,7 @@ const verifyEndpoint = (encoded) => decodeAdminVerifyRequest(encoded).pipe(Effec
|
|
|
983
1102
|
const retryEndpoint = (encoded) => decodeRetryCommand(encoded).pipe(Effect.mapError(protocolFailure("The retry command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
984
1103
|
const maintenance = yield* ConversationMaintenance;
|
|
985
1104
|
const runtime = yield* DurableAgentRuntime;
|
|
986
|
-
yield* maintenance.
|
|
987
|
-
const report = yield* runtime.retry(command);
|
|
1105
|
+
const report = yield* maintenance.withMutation(runtime.retry(command));
|
|
988
1106
|
return RetryExecuted.make({ report });
|
|
989
1107
|
})), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
|
|
990
1108
|
const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pipe(Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")), Effect.flatMap((thresholds) => Effect.gen(function* () {
|
|
@@ -992,20 +1110,20 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
|
|
|
992
1110
|
return ObligationsScanned.make({ report });
|
|
993
1111
|
})), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
|
|
994
1112
|
/**
|
|
995
|
-
* Owner-side `portCall`:
|
|
996
|
-
* THIS Object must already carry the alarm that will
|
|
997
|
-
* (never the routed decorators), then arm an immediate
|
|
998
|
-
* processed promptly. Protocol anomalies answer
|
|
1113
|
+
* Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
|
|
1114
|
+
* public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
|
|
1115
|
+
* finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
|
|
1116
|
+
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
1117
|
+
* `PortFailed(PortProtocolError)`.
|
|
999
1118
|
*/
|
|
1000
1119
|
const portCallEndpoint = (encoded) => Effect.gen(function* () {
|
|
1001
1120
|
const ports = yield* ConversationObjectPorts;
|
|
1002
1121
|
const maintenance = yield* ConversationMaintenance;
|
|
1003
1122
|
const alarm = yield* DurableAlarmService;
|
|
1004
1123
|
const mutating = isMutatingPortRequest(encoded);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const response = yield* ports.handle(encoded);
|
|
1124
|
+
const handled = yield* (mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)).pipe(Effect.exit);
|
|
1125
|
+
if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
|
|
1126
|
+
const response = handled.value;
|
|
1009
1127
|
if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error)));
|
|
1010
1128
|
return response;
|
|
1011
1129
|
});
|
|
@@ -1711,6 +1829,6 @@ const classifyWorkerFailure = (cause, maxWallTime) => {
|
|
|
1711
1829
|
/** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
|
|
1712
1830
|
const dynamicWorkerCodeExecutorLayer = (options) => Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
|
|
1713
1831
|
//#endregion
|
|
1714
|
-
export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareAdmissionLimitsValue, CloudflareBindingError, CloudflareConversationClient, CloudflareDurableRuntime, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError, CodeModeHostEntrypoint, ConversationClientError, ConversationMaintenance, ConversationObjectIdentity, ConversationObjectNamespace, ConversationObjectPorts, DEFAULT_MAX_DATABASE_BYTES, DurableAlarmError, DurableAlarmService, DurableObjectContext, ExplainedRecovery, HostFailed, HostFailure, HostProtocolError, HostResponse, MaintenancePassReport, ObligationsScanned, ObservePageRequest, ObservedPage, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
|
|
1832
|
+
export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareAdmissionLimitsValue, CloudflareBindingError, CloudflareConversationClient, CloudflareDurableRuntime, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError, CodeModeHostEntrypoint, ConversationClientError, ConversationMaintenance, ConversationMaintenanceFailpoint, ConversationObjectIdentity, ConversationObjectNamespace, ConversationObjectPorts, DEFAULT_MAX_DATABASE_BYTES, DurableAlarmError, DurableAlarmService, DurableObjectContext, ExplainedRecovery, HostFailed, HostFailure, HostProtocolError, HostResponse, MaintenancePassReport, ObligationsScanned, ObservePageRequest, ObservedPage, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
|
|
1715
1833
|
|
|
1716
1834
|
//# sourceMappingURL=index.mjs.map
|