@effect-agent/platform-cloudflare 0.1.0-beta.14 → 0.1.0-beta.16
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 +192 -120
- package/dist/index.mjs +351 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/alarm.ts +330 -113
- package/src/bindings.ts +4 -0
- package/src/client.ts +150 -2
- package/src/conversation-object.ts +87 -67
- package/src/index.ts +4 -2
- package/src/layers.ts +39 -7
- package/src/progress-wait.ts +103 -0
- package/src/wake-scheduler.ts +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
|
|
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";
|
|
1
|
+
import { Cause, Clock, Context, Crypto, Deferred, Duration, Effect, Exit, Fiber, Layer, Option, PubSub, Random, Ref, Schema, Semaphore, Stream } from "effect";
|
|
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, makeWakeSubscriptionHub, operationAuthorizerLayer } 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";
|
|
5
5
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
@@ -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
|
};
|
|
@@ -375,8 +497,9 @@ const cloudflareWakeSchedulerLayer = Layer.effect(WakeScheduler)(Effect.gen(func
|
|
|
375
497
|
const identity = yield* ConversationObjectIdentity;
|
|
376
498
|
const { namespace } = yield* ConversationObjectNamespace;
|
|
377
499
|
const hints = yield* PubSub.sliding(WAKE_BUFFER_CAPACITY);
|
|
500
|
+
const progress = yield* makeWakeSubscriptionHub;
|
|
378
501
|
yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
|
|
379
|
-
const notifyLocal = (conversationId) => PubSub.publish(hints, conversationId)
|
|
502
|
+
const notifyLocal = (conversationId) => progress.notify(conversationId).pipe(Effect.andThen(PubSub.publish(hints, conversationId)), Effect.andThen(alarm.scheduleNow), Effect.catch((error) => Effect.logWarning("CloudflareWakeScheduler: local alarm wake failed", error)), Effect.asVoid);
|
|
380
503
|
const notifyRemote = (conversationId) => Effect.tryPromise({
|
|
381
504
|
try: () => namespace.get(namespace.idFromName(conversationId)).wake(),
|
|
382
505
|
catch: (cause) => RemoteWakeDropped.make({
|
|
@@ -387,10 +510,76 @@ const cloudflareWakeSchedulerLayer = Layer.effect(WakeScheduler)(Effect.gen(func
|
|
|
387
510
|
}).pipe(Effect.catch((error) => Effect.logWarning(`CloudflareWakeScheduler: remote wake of ${conversationId} dropped`, error)), Effect.asVoid);
|
|
388
511
|
return WakeScheduler.of({
|
|
389
512
|
notify: (conversationId) => conversationId === identity.conversationId ? notifyLocal(conversationId) : notifyRemote(conversationId),
|
|
513
|
+
subscribe: progress.subscribe,
|
|
390
514
|
wakes: Stream.fromPubSub(hints)
|
|
391
515
|
});
|
|
392
516
|
}));
|
|
393
517
|
//#endregion
|
|
518
|
+
//#region src/progress-wait.ts
|
|
519
|
+
/** Cancellation tombstones are bounded hints, never durable authority. */
|
|
520
|
+
const MAX_CANCELLATION_TOMBSTONES = 1024;
|
|
521
|
+
/**
|
|
522
|
+
* Per-incarnation cancellation registry for long-lived progress RPCs. The public runtime owns
|
|
523
|
+
* the actual wake registration; this host-only registry lets an interrupted Worker Effect ask
|
|
524
|
+
* the Object to interrupt its scoped wait before the Worker execution context itself ends.
|
|
525
|
+
*/
|
|
526
|
+
var ProgressWaitRegistry = class ProgressWaitRegistry extends Context.Service()("@effect-agent/platform-cloudflare/ProgressWaitRegistry") {
|
|
527
|
+
static layer = Layer.effect(ProgressWaitRegistry, Effect.gen(function* () {
|
|
528
|
+
const registrations = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
529
|
+
const remove = (waiterId, deferred) => Ref.update(registrations, (current) => {
|
|
530
|
+
const existing = current.get(waiterId);
|
|
531
|
+
if (existing === void 0 || existing === "cancelled" || !existing.has(deferred)) return current;
|
|
532
|
+
const next = new Map(current);
|
|
533
|
+
const active = new Set(existing);
|
|
534
|
+
active.delete(deferred);
|
|
535
|
+
if (active.size === 0) next.delete(waiterId);
|
|
536
|
+
else next.set(waiterId, active);
|
|
537
|
+
return next;
|
|
538
|
+
});
|
|
539
|
+
const subscribe = Effect.fn("ProgressWaitRegistry.subscribe")((waiterId) => Effect.gen(function* () {
|
|
540
|
+
const deferred = yield* Deferred.make();
|
|
541
|
+
yield* Effect.addFinalizer(() => remove(waiterId, deferred));
|
|
542
|
+
return {
|
|
543
|
+
cancelled: yield* Ref.modify(registrations, (current) => {
|
|
544
|
+
const existing = current.get(waiterId);
|
|
545
|
+
const next = new Map(current);
|
|
546
|
+
if (existing === "cancelled") return [true, current];
|
|
547
|
+
const active = new Set(existing ?? []);
|
|
548
|
+
active.add(deferred);
|
|
549
|
+
next.set(waiterId, active);
|
|
550
|
+
return [false, next];
|
|
551
|
+
}),
|
|
552
|
+
deferred
|
|
553
|
+
};
|
|
554
|
+
}).pipe(Effect.map(({ cancelled, deferred }) => cancelled ? Effect.void : Deferred.await(deferred))));
|
|
555
|
+
const cancel = Effect.fn("ProgressWaitRegistry.cancel")(function* (waiterId) {
|
|
556
|
+
const waiters = yield* Ref.modify(registrations, (current) => {
|
|
557
|
+
const existing = current.get(waiterId);
|
|
558
|
+
const next = new Map(current);
|
|
559
|
+
if (existing === void 0) {
|
|
560
|
+
next.set(waiterId, "cancelled");
|
|
561
|
+
let tombstones = 0;
|
|
562
|
+
for (const registration of next.values()) if (registration === "cancelled") tombstones += 1;
|
|
563
|
+
if (tombstones > MAX_CANCELLATION_TOMBSTONES) for (const [id, registration] of next) {
|
|
564
|
+
if (registration !== "cancelled") continue;
|
|
565
|
+
next.delete(id);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
return [[], next];
|
|
569
|
+
}
|
|
570
|
+
if (existing === "cancelled") return [[], current];
|
|
571
|
+
next.delete(waiterId);
|
|
572
|
+
return [[...existing], next];
|
|
573
|
+
});
|
|
574
|
+
yield* Effect.forEach(waiters, (waiter) => Deferred.succeed(waiter, void 0), { discard: true });
|
|
575
|
+
});
|
|
576
|
+
return ProgressWaitRegistry.of({
|
|
577
|
+
subscribe,
|
|
578
|
+
cancel
|
|
579
|
+
});
|
|
580
|
+
}));
|
|
581
|
+
};
|
|
582
|
+
//#endregion
|
|
394
583
|
//#region src/transport.ts
|
|
395
584
|
/**
|
|
396
585
|
* `ConversationPortTransport` over native Durable Object JS RPC (decision D-P6-3): one
|
|
@@ -519,15 +708,17 @@ var CloudflareDurableRuntime = class {
|
|
|
519
708
|
abortPollInterval: Duration.millis(config.abortPollInterval)
|
|
520
709
|
}));
|
|
521
710
|
const runtimeFailpointLayer = options.runtimeFailpoint === void 0 ? DurableRuntimeFailpoint.layer : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
|
|
711
|
+
const maintenanceFailpointLayer = options.maintenanceFailpoint === void 0 ? ConversationMaintenanceFailpoint.layer : Layer.succeed(ConversationMaintenanceFailpoint)({ hit: options.maintenanceFailpoint(ctx) });
|
|
522
712
|
const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
|
|
713
|
+
const authorizerLayer = options.operationAuthorizer === void 0 ? Layer.empty : operationAuthorizerLayer(options.operationAuthorizer);
|
|
523
714
|
const bindingResolverLayer = Layer.effect(AgentBindingResolver)(Effect.map(resolveBindings(options.bindings, {
|
|
524
715
|
ctx,
|
|
525
716
|
env,
|
|
526
717
|
conversationId,
|
|
527
718
|
producerId
|
|
528
719
|
}), (bindings) => AgentBindingResolver.fromBindings(bindings)));
|
|
529
|
-
const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer);
|
|
530
|
-
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));
|
|
720
|
+
const base = Layer.mergeAll(identityLayer, cloudflareConfigLayer, DurableAlarmService.layer, maintenanceFailpointLayer, ProgressWaitRegistry.layer);
|
|
721
|
+
const runtimeStack = DurableAgentRuntime.layer.pipe(Layer.provideMerge(routedPorts), Layer.provideMerge(cloudflareWakeSchedulerLayer), Layer.provideMerge(runtimeConfigLayer), Layer.provideMerge(bindingResolverLayer), Layer.provide(Layer.mergeAll(runtimeFailpointLayer, reconcilerLayer, authorizerLayer, BrowserCrypto.layer)), Layer.provideMerge(base));
|
|
531
722
|
return Layer.mergeAll(runtimeStack, ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)), portsEndpointLayer);
|
|
532
723
|
}));
|
|
533
724
|
}
|
|
@@ -556,7 +747,11 @@ var HostProtocolError = class extends Schema.TaggedError()("HostProtocolError",
|
|
|
556
747
|
var ConversationClientError = class extends Schema.TaggedError()("ConversationClientError", {
|
|
557
748
|
conversationId: Schema.String,
|
|
558
749
|
message: Schema.String,
|
|
559
|
-
cause: Schema.optionalKey(Schema.Defect())
|
|
750
|
+
cause: Schema.optionalKey(Schema.Defect()),
|
|
751
|
+
/** Cloudflare's own classification for a failure safe to retry with a fresh stub. */
|
|
752
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
753
|
+
/** Cloudflare overloads are surfaced immediately instead of adding retry pressure. */
|
|
754
|
+
overloaded: Schema.optionalKey(Schema.Boolean)
|
|
560
755
|
}) {};
|
|
561
756
|
/**
|
|
562
757
|
* One durable submission, input ALREADY encoded by the caller through the Agent Binding's
|
|
@@ -576,6 +771,13 @@ var ObservePageRequest = class extends Schema.Class("@effect-agent/platform-clou
|
|
|
576
771
|
afterSequence: Schema.optionalKey(CanonicalSequence),
|
|
577
772
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
578
773
|
}) {};
|
|
774
|
+
/** One event-driven wait for canonical progress strictly after this sequence. */
|
|
775
|
+
var AwaitProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/AwaitProgressRequest")({
|
|
776
|
+
afterSequence: CanonicalSequence,
|
|
777
|
+
waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256))
|
|
778
|
+
}) {};
|
|
779
|
+
/** Best-effort cancellation of one in-flight progress RPC. */
|
|
780
|
+
var CancelProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/CancelProgressRequest")({ waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)) }) {};
|
|
579
781
|
/**
|
|
580
782
|
* Every typed failure a host entry point can produce, plus the protocol's own errors. Same
|
|
581
783
|
* closed-union discipline as the WP2 `PortFailure`: members re-decode to the SAME tagged
|
|
@@ -598,11 +800,15 @@ const HostFailure = Schema.Union([
|
|
|
598
800
|
DurableRuntimeFailpointError,
|
|
599
801
|
AdmissionLimitExceeded,
|
|
600
802
|
DurableAlarmError,
|
|
803
|
+
OperationDenied,
|
|
601
804
|
HostProtocolError
|
|
602
805
|
]);
|
|
603
806
|
var SubmitSucceeded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SubmitSucceeded")("SubmitSucceeded", { receipt: Receipt }) {};
|
|
604
807
|
var SettlementReached = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SettlementReached")("SettlementReached", { settlement: Settlement }) {};
|
|
605
808
|
var ObservedPage = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ObservedPage")("ObservedPage", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
|
|
809
|
+
/** A record was already committed or an incarnation-local hint says the caller should re-read. */
|
|
810
|
+
var ProgressObserved = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressObserved")("ProgressObserved", {}) {};
|
|
811
|
+
var ProgressCancelled = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressCancelled")("ProgressCancelled", {}) {};
|
|
606
812
|
var AbortRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/AbortRecorded")("AbortRecorded", { intent: AbortIntent }) {};
|
|
607
813
|
var ApprovalRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ApprovalRecorded")("ApprovalRecorded", { intent: ApprovalDecisionIntent }) {};
|
|
608
814
|
var UnknownResolutionRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/UnknownResolutionRecorded")("UnknownResolutionRecorded", { intent: UnknownResolutionIntent }) {};
|
|
@@ -613,6 +819,8 @@ const HostResponse = Schema.Union([
|
|
|
613
819
|
SubmitSucceeded,
|
|
614
820
|
SettlementReached,
|
|
615
821
|
ObservedPage,
|
|
822
|
+
ProgressObserved,
|
|
823
|
+
ProgressCancelled,
|
|
616
824
|
AbortRecorded,
|
|
617
825
|
ApprovalRecorded,
|
|
618
826
|
UnknownResolutionRecorded,
|
|
@@ -624,6 +832,10 @@ const decodeReceipt = Schema.decodeUnknownEffect(Receipt);
|
|
|
624
832
|
const encodeReceipt = Schema.encodeEffect(Receipt);
|
|
625
833
|
const decodeObservePageRequest = Schema.decodeUnknownEffect(ObservePageRequest);
|
|
626
834
|
const encodeObservePageRequest = Schema.encodeEffect(ObservePageRequest);
|
|
835
|
+
const decodeAwaitProgressRequest = Schema.decodeUnknownEffect(AwaitProgressRequest);
|
|
836
|
+
const encodeAwaitProgressRequest = Schema.encodeEffect(AwaitProgressRequest);
|
|
837
|
+
const decodeCancelProgressRequest = Schema.decodeUnknownEffect(CancelProgressRequest);
|
|
838
|
+
const encodeCancelProgressRequest = Schema.encodeEffect(CancelProgressRequest);
|
|
627
839
|
const decodeAbortCommand = Schema.decodeUnknownEffect(AbortCommand);
|
|
628
840
|
const encodeAbortCommand = Schema.encodeEffect(AbortCommand);
|
|
629
841
|
const decodeApprovalDecisionCommand = Schema.decodeUnknownEffect(ApprovalDecisionCommand);
|
|
@@ -654,8 +866,10 @@ const AWAIT_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
|
654
866
|
const OBSERVE_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
655
867
|
"ConversationStoreError",
|
|
656
868
|
"ConversationNotMaterialized",
|
|
869
|
+
"OperationDenied",
|
|
657
870
|
"HostProtocolError"
|
|
658
871
|
]);
|
|
872
|
+
const PROGRESS_FAILURE_TAGS = OBSERVE_FAILURE_TAGS;
|
|
659
873
|
const ABORT_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
660
874
|
"LedgerError",
|
|
661
875
|
"SettlementConflict",
|
|
@@ -668,6 +882,7 @@ const APPROVAL_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
|
668
882
|
"LedgerError",
|
|
669
883
|
"SettlementConflict",
|
|
670
884
|
"ApprovalConflict",
|
|
885
|
+
"OperationDenied",
|
|
671
886
|
"DurableAlarmError",
|
|
672
887
|
"HostProtocolError"
|
|
673
888
|
]);
|
|
@@ -677,6 +892,7 @@ const UNKNOWN_FAILURE_TAGS = /* @__PURE__ */ new Set([
|
|
|
677
892
|
"UnknownResolutionConflict",
|
|
678
893
|
"JoinedToHost",
|
|
679
894
|
"DurableRuntimeFailpointError",
|
|
895
|
+
"OperationDenied",
|
|
680
896
|
"DurableAlarmError",
|
|
681
897
|
"HostProtocolError"
|
|
682
898
|
]);
|
|
@@ -695,12 +911,27 @@ const narrowFailure = (tags) => (failure) => tags.has(failure._tag);
|
|
|
695
911
|
var CloudflareConversationClient = class CloudflareConversationClient extends Context.Service()("@effect-agent/platform-cloudflare/CloudflareConversationClient") {
|
|
696
912
|
static layer = Layer.effect(CloudflareConversationClient)(Effect.gen(function* () {
|
|
697
913
|
const { namespace } = yield* ConversationObjectNamespace;
|
|
914
|
+
const crypto = yield* Crypto.Crypto;
|
|
915
|
+
const platformSignals = (cause) => {
|
|
916
|
+
let retryable;
|
|
917
|
+
let overloaded;
|
|
918
|
+
if (typeof cause === "object" && cause !== null) {
|
|
919
|
+
if ("retryable" in cause && typeof cause.retryable === "boolean") retryable = cause.retryable;
|
|
920
|
+
if ("overloaded" in cause && typeof cause.overloaded === "boolean") overloaded = cause.overloaded;
|
|
921
|
+
if (retryable === void 0 && "durableObjectReset" in cause && cause.durableObjectReset === true) retryable = true;
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
...retryable === void 0 ? {} : { retryable },
|
|
925
|
+
...overloaded === void 0 ? {} : { overloaded }
|
|
926
|
+
};
|
|
927
|
+
};
|
|
698
928
|
const call = (conversationId, operation, invoke) => Effect.tryPromise({
|
|
699
929
|
try: () => invoke(namespace.get(namespace.idFromName(conversationId))),
|
|
700
930
|
catch: (cause) => ConversationClientError.make({
|
|
701
931
|
conversationId,
|
|
702
932
|
message: boundHostDiagnostic(`${operation} did not reach the Conversation Object: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
703
|
-
cause
|
|
933
|
+
cause,
|
|
934
|
+
...platformSignals(cause)
|
|
704
935
|
})
|
|
705
936
|
}).pipe(Effect.flatMap((raw) => decodeHostResponse(raw).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`The ${operation} answer could not be decoded: ${error.message}`) })))), Effect.withSpan("CloudflareConversationClient.call", { attributes: {
|
|
706
937
|
conversationId,
|
|
@@ -726,6 +957,7 @@ var CloudflareConversationClient = class CloudflareConversationClient extends Co
|
|
|
726
957
|
const response = yield* call(conversationId, "observePage", (stub) => stub.observePage(encoded));
|
|
727
958
|
return (yield* expect(conversationId, "observePage", "ObservedPage", OBSERVE_FAILURE_TAGS)(response)).records;
|
|
728
959
|
});
|
|
960
|
+
const cancelProgress = (conversationId, waiterId) => encodeCancelProgressRequest(CancelProgressRequest.make({ waiterId })).pipe(Effect.mapError(() => void 0), Effect.flatMap((encoded) => call(conversationId, "cancelProgress", (stub) => stub.cancelProgressEncoded(encoded))), Effect.asVoid, Effect.ignore);
|
|
729
961
|
return CloudflareConversationClient.of({
|
|
730
962
|
submit: (agent, input, options) => Effect.gen(function* () {
|
|
731
963
|
const encodedInput = yield* Schema.encodeEffect(agent.definition.input)(input).pipe(Effect.mapError((cause) => AgentInputError.make({ message: `Unable to encode Agent input: ${cause.message}` })));
|
|
@@ -746,6 +978,16 @@ var CloudflareConversationClient = class CloudflareConversationClient extends Co
|
|
|
746
978
|
const response = yield* call(receipt.conversationId, "awaitSettlement", (stub) => stub.awaitSettlementEncoded(encoded));
|
|
747
979
|
return (yield* expect(receipt.conversationId, "awaitSettlement", "SettlementReached", AWAIT_FAILURE_TAGS)(response)).settlement;
|
|
748
980
|
}),
|
|
981
|
+
awaitProgress: (conversationId, afterSequence) => Effect.gen(function* () {
|
|
982
|
+
const waiterId = yield* crypto.randomUUIDv4.pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress cancellation identity generation failed: ${error.message}`) })));
|
|
983
|
+
const request = AwaitProgressRequest.make({
|
|
984
|
+
afterSequence,
|
|
985
|
+
waiterId
|
|
986
|
+
});
|
|
987
|
+
const encoded = yield* encodeAwaitProgressRequest(request).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress request encode failed: ${error.message}`) })));
|
|
988
|
+
const attempt = (retry) => call(conversationId, "awaitProgress", (stub) => stub.awaitProgressEncoded(encoded)).pipe(Effect.flatMap(expect(conversationId, "awaitProgress", "ProgressObserved", PROGRESS_FAILURE_TAGS)), Effect.asVoid, Effect.catchTag("ConversationClientError", (error) => error.retryable === true && error.overloaded !== true && retry < 5 ? Effect.sleep(Duration.millis(10 * 2 ** retry)).pipe(Effect.andThen(attempt(retry + 1))) : Effect.fail(error)));
|
|
989
|
+
yield* attempt(0).pipe(Effect.onInterrupt(() => cancelProgress(conversationId, waiterId)));
|
|
990
|
+
}),
|
|
749
991
|
readPage,
|
|
750
992
|
readAll: (conversationId) => Effect.gen(function* () {
|
|
751
993
|
const all = [];
|
|
@@ -862,26 +1104,39 @@ const submitEndpoint = (encoded) => decodeSubmitRequest(encoded).pipe(Effect.map
|
|
|
862
1104
|
const maintenance = yield* ConversationMaintenance;
|
|
863
1105
|
const runtime = yield* DurableAgentRuntime;
|
|
864
1106
|
yield* gateAdmissionLimits(request);
|
|
865
|
-
yield* maintenance.
|
|
866
|
-
const receipt = yield* runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
1107
|
+
const receipt = yield* maintenance.withMutation(runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
|
|
867
1108
|
conversationId: identity.conversationId,
|
|
868
1109
|
principal: request.principal,
|
|
869
1110
|
idempotencyKey: request.idempotencyKey,
|
|
870
1111
|
definitions: request.definitions
|
|
871
|
-
});
|
|
1112
|
+
}));
|
|
872
1113
|
return SubmitSucceeded.make({ receipt });
|
|
873
1114
|
})), respond, Effect.flatMap(encodeResponse));
|
|
874
1115
|
const awaitSettlementEndpoint = (encoded) => decodeReceipt(encoded).pipe(Effect.mapError(protocolFailure("The receipt could not be decoded")), Effect.flatMap((receipt) => Effect.gen(function* () {
|
|
875
1116
|
const settlement = yield* (yield* DurableAgentRuntime).awaitSettlement(receipt);
|
|
876
1117
|
return SettlementReached.make({ settlement });
|
|
877
1118
|
})), respond, Effect.flatMap(encodeResponse));
|
|
1119
|
+
const awaitProgressEndpoint = (encoded) => decodeAwaitProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
1120
|
+
const identity = yield* ConversationObjectIdentity;
|
|
1121
|
+
const runtime = yield* DurableAgentRuntime;
|
|
1122
|
+
const registry = yield* ProgressWaitRegistry;
|
|
1123
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
1124
|
+
const cancelled = yield* registry.subscribe(request.waiterId);
|
|
1125
|
+
yield* Effect.raceFirst(runtime.awaitProgress(identity.conversationId, request.afterSequence), cancelled);
|
|
1126
|
+
}));
|
|
1127
|
+
return ProgressObserved.make();
|
|
1128
|
+
})), respond, Effect.flatMap(encodeResponse));
|
|
1129
|
+
const cancelProgressEndpoint = (encoded) => decodeCancelProgressRequest(encoded).pipe(Effect.mapError(protocolFailure("The progress cancellation could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
1130
|
+
yield* (yield* ProgressWaitRegistry).cancel(request.waiterId);
|
|
1131
|
+
return ProgressCancelled.make();
|
|
1132
|
+
})), respond, Effect.flatMap(encodeResponse));
|
|
878
1133
|
const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(Effect.mapError(protocolFailure("The observe request could not be decoded")), Effect.flatMap((request) => Effect.gen(function* () {
|
|
879
1134
|
const identity = yield* ConversationObjectIdentity;
|
|
880
1135
|
const store = yield* ConversationStore;
|
|
881
1136
|
yield* (yield* OperationAuthorizer).authorize(OperationAuthorizationRequest.make({
|
|
882
1137
|
operation: "observe",
|
|
883
1138
|
conversationId: identity.conversationId
|
|
884
|
-
}))
|
|
1139
|
+
}));
|
|
885
1140
|
const records = yield* Stream.runCollect(store.read(ConversationRead.make({
|
|
886
1141
|
conversationId: identity.conversationId,
|
|
887
1142
|
...request.afterSequence === void 0 ? {} : { afterSequence: request.afterSequence },
|
|
@@ -892,30 +1147,19 @@ const observePageEndpoint = (encoded) => decodeObservePageRequest(encoded).pipe(
|
|
|
892
1147
|
const abortEndpoint = (encoded) => decodeAbortCommand(encoded).pipe(Effect.mapError(protocolFailure("The abort command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
893
1148
|
const maintenance = yield* ConversationMaintenance;
|
|
894
1149
|
const runtime = yield* DurableAgentRuntime;
|
|
895
|
-
yield* maintenance.
|
|
896
|
-
const intent = yield* runtime.abort(command);
|
|
1150
|
+
const intent = yield* maintenance.withMutation(runtime.abort(command));
|
|
897
1151
|
return AbortRecorded.make({ intent });
|
|
898
1152
|
})), respond, Effect.flatMap(encodeResponse));
|
|
899
|
-
/**
|
|
900
|
-
* The pre-P7 host protocol's failure union does not carry `OperationDenied` (the Worker client
|
|
901
|
-
* predates the authorizer). This assembly always runs the default possession authorizer — no
|
|
902
|
-
* `CloudflareDurableRuntimeOptions` authorizer lever exists yet — so a denial here is
|
|
903
|
-
* unreachable today; if one ever surfaces it degrades to the protocol failure instead of an
|
|
904
|
-
* out-of-contract throw. The four P7 admin entry points below carry `OperationDenied` typed.
|
|
905
|
-
*/
|
|
906
|
-
const deniedToProtocolFailure = (denied) => Effect.fail(HostProtocolError.make({ message: boundHostDiagnostic(`The ${denied.operation} operation was denied: ${denied.reason}`) }));
|
|
907
1153
|
const resolveApprovalEndpoint = (encoded) => decodeApprovalDecisionCommand(encoded).pipe(Effect.mapError(protocolFailure("The approval command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
908
1154
|
const maintenance = yield* ConversationMaintenance;
|
|
909
1155
|
const runtime = yield* DurableAgentRuntime;
|
|
910
|
-
yield* maintenance.
|
|
911
|
-
const intent = yield* runtime.resolveApproval(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
1156
|
+
const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
|
|
912
1157
|
return ApprovalRecorded.make({ intent });
|
|
913
1158
|
})), respond, Effect.flatMap(encodeResponse));
|
|
914
1159
|
const resolveUnknownEndpoint = (encoded) => decodeUnknownResolutionCommand(encoded).pipe(Effect.mapError(protocolFailure("The resolution command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
915
1160
|
const maintenance = yield* ConversationMaintenance;
|
|
916
1161
|
const runtime = yield* DurableAgentRuntime;
|
|
917
|
-
yield* maintenance.
|
|
918
|
-
const intent = yield* runtime.resolveUnknown(command).pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
1162
|
+
const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
|
|
919
1163
|
return UnknownResolutionRecorded.make({ intent });
|
|
920
1164
|
})), respond, Effect.flatMap(encodeResponse));
|
|
921
1165
|
/** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
|
|
@@ -983,8 +1227,7 @@ const verifyEndpoint = (encoded) => decodeAdminVerifyRequest(encoded).pipe(Effec
|
|
|
983
1227
|
const retryEndpoint = (encoded) => decodeRetryCommand(encoded).pipe(Effect.mapError(protocolFailure("The retry command could not be decoded")), Effect.flatMap((command) => Effect.gen(function* () {
|
|
984
1228
|
const maintenance = yield* ConversationMaintenance;
|
|
985
1229
|
const runtime = yield* DurableAgentRuntime;
|
|
986
|
-
yield* maintenance.
|
|
987
|
-
const report = yield* runtime.retry(command);
|
|
1230
|
+
const report = yield* maintenance.withMutation(runtime.retry(command));
|
|
988
1231
|
return RetryExecuted.make({ report });
|
|
989
1232
|
})), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
|
|
990
1233
|
const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pipe(Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")), Effect.flatMap((thresholds) => Effect.gen(function* () {
|
|
@@ -992,25 +1235,26 @@ const obligationsEndpoint = (encoded) => decodeObligationThresholds(encoded).pip
|
|
|
992
1235
|
return ObligationsScanned.make({ report });
|
|
993
1236
|
})), respondAdmin, Effect.flatMap(encodeAdminResponseTotal));
|
|
994
1237
|
/**
|
|
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
|
|
1238
|
+
* Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
|
|
1239
|
+
* public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
|
|
1240
|
+
* finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
|
|
1241
|
+
* alarm so the mutated lane is processed promptly. Protocol anomalies answer
|
|
1242
|
+
* `PortFailed(PortProtocolError)`.
|
|
999
1243
|
*/
|
|
1000
1244
|
const portCallEndpoint = (encoded) => Effect.gen(function* () {
|
|
1001
1245
|
const ports = yield* ConversationObjectPorts;
|
|
1002
1246
|
const maintenance = yield* ConversationMaintenance;
|
|
1003
1247
|
const alarm = yield* DurableAlarmService;
|
|
1004
1248
|
const mutating = isMutatingPortRequest(encoded);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const response = yield* ports.handle(encoded);
|
|
1249
|
+
const handled = yield* (mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)).pipe(Effect.exit);
|
|
1250
|
+
if (handled._tag === "Failure") return encodedPortProtocolFailure("The owner Object could not arm its maintenance alarm before the mutation.");
|
|
1251
|
+
const response = handled.value;
|
|
1009
1252
|
if (mutating) yield* alarm.scheduleNow.pipe(Effect.catch((error) => Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error)));
|
|
1010
1253
|
return response;
|
|
1011
1254
|
});
|
|
1012
1255
|
const wakeEndpoint = Effect.gen(function* () {
|
|
1013
|
-
|
|
1256
|
+
const identity = yield* ConversationObjectIdentity;
|
|
1257
|
+
yield* (yield* WakeScheduler).notify(identity.conversationId);
|
|
1014
1258
|
});
|
|
1015
1259
|
const alarmEndpoint = Effect.gen(function* () {
|
|
1016
1260
|
yield* (yield* ConversationMaintenance).pass;
|
|
@@ -1059,6 +1303,8 @@ const makeConversationObjectClass = (options, observability) => {
|
|
|
1059
1303
|
const rpc = {
|
|
1060
1304
|
submitEncoded: (encoded) => submitEndpoint(encoded),
|
|
1061
1305
|
awaitSettlementEncoded: (encoded) => awaitSettlementEndpoint(encoded),
|
|
1306
|
+
awaitProgressEncoded: (encoded) => awaitProgressEndpoint(encoded),
|
|
1307
|
+
cancelProgressEncoded: (encoded) => cancelProgressEndpoint(encoded),
|
|
1062
1308
|
observePage: (encoded) => observePageEndpoint(encoded),
|
|
1063
1309
|
abortEncoded: (encoded) => abortEndpoint(encoded),
|
|
1064
1310
|
resolveApprovalEncoded: (encoded) => resolveApprovalEndpoint(encoded),
|
|
@@ -1711,6 +1957,6 @@ const classifyWorkerFailure = (cause, maxWallTime) => {
|
|
|
1711
1957
|
/** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
|
|
1712
1958
|
const dynamicWorkerCodeExecutorLayer = (options) => Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
|
|
1713
1959
|
//#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 };
|
|
1960
|
+
export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, AwaitProgressRequest, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CancelProgressRequest, 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, ProgressCancelled, ProgressObserved, ProgressWaitRegistry, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeAwaitProgressRequest, decodeCancelProgressRequest, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeAwaitProgressRequest, encodeCancelProgressRequest, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
|
|
1715
1961
|
|
|
1716
1962
|
//# sourceMappingURL=index.mjs.map
|