@effect-agent/platform-cloudflare 0.1.0-beta.45 → 0.1.0-beta.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/Alarm.d.mts +119 -0
  2. package/dist/Alarm.mjs +317 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/{browser-rest-capture.d.mts → BrowserRestCapture.d.mts} +5 -4
  5. package/dist/{browser-rest-capture.mjs → BrowserRestCapture.mjs} +5 -4
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/{browser-rest-crawl.d.mts → BrowserRestCrawl.d.mts} +4 -3
  8. package/dist/{browser-rest-crawl.mjs → BrowserRestCrawl.mjs} +4 -3
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/{bindings-BrVEOkfe.d.mts → CloudflareBindings.d.mts} +7 -5
  11. package/dist/CloudflareBindings.mjs +98 -0
  12. package/dist/CloudflareBindings.mjs.map +1 -0
  13. package/dist/{browser-quick-action.mjs → CloudflareBrowser-1jeZx3_g.mjs} +19 -4
  14. package/dist/CloudflareBrowser-1jeZx3_g.mjs.map +1 -0
  15. package/dist/{browser-quick-action.d.mts → CloudflareBrowser-DJq53niJ.d.mts} +9 -18
  16. package/dist/CloudflareBrowser.d.mts +2 -0
  17. package/dist/CloudflareBrowser.mjs +2 -0
  18. package/dist/CloudflareCodeMode.d.mts +44 -0
  19. package/dist/CloudflareCodeMode.mjs +618 -0
  20. package/dist/CloudflareCodeMode.mjs.map +1 -0
  21. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  22. package/dist/CloudflareConfig.d.mts +2 -0
  23. package/dist/CloudflareConfig.mjs +122 -0
  24. package/dist/CloudflareConfig.mjs.map +1 -0
  25. package/dist/CloudflareMemory.d.mts +140 -0
  26. package/dist/CloudflareMemory.mjs +168 -0
  27. package/dist/CloudflareMemory.mjs.map +1 -0
  28. package/dist/{scheduling.d.mts → CloudflareScheduling.d.mts} +9 -6
  29. package/dist/{scheduling.mjs → CloudflareScheduling.mjs} +22 -8
  30. package/dist/CloudflareScheduling.mjs.map +1 -0
  31. package/dist/{subscriptions.d.mts → CloudflareSubscriptions.d.mts} +11 -6
  32. package/dist/{subscriptions.mjs → CloudflareSubscriptions.mjs} +26 -7
  33. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  34. package/dist/CloudflareThreadClient.d.mts +715 -0
  35. package/dist/CloudflareThreadClient.mjs +364 -0
  36. package/dist/CloudflareThreadClient.mjs.map +1 -0
  37. package/dist/{interactive-browser.d.mts → InteractiveBrowser.d.mts} +5 -4
  38. package/dist/{interactive-browser.mjs → InteractiveBrowser.mjs} +6 -4
  39. package/dist/InteractiveBrowser.mjs.map +1 -0
  40. package/dist/{protected-browser.d.mts → ProtectedBrowser.d.mts} +4 -3
  41. package/dist/{protected-browser.mjs → ProtectedBrowser.mjs} +4 -3
  42. package/dist/ProtectedBrowser.mjs.map +1 -0
  43. package/dist/ThreadObject-IrGuIO4a.mjs +575 -0
  44. package/dist/ThreadObject-IrGuIO4a.mjs.map +1 -0
  45. package/dist/ThreadObject-TEgOO0YM.d.mts +735 -0
  46. package/dist/ThreadObject.d.mts +2 -0
  47. package/dist/ThreadObject.mjs +3 -0
  48. package/dist/WakeScheduler.d.mts +23 -0
  49. package/dist/WakeScheduler.mjs +60 -0
  50. package/dist/WakeScheduler.mjs.map +1 -0
  51. package/dist/boundary-BguazVkh.mjs +42 -0
  52. package/dist/boundary-BguazVkh.mjs.map +1 -0
  53. package/dist/{browser-session-lifecycle-ZGgb3pnK.mjs → browser-session-lifecycle-CUbVfEgP.mjs} +2 -2
  54. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  55. package/dist/{browser-session-lifecycle-DqntvG-Y.d.mts → browser-session-lifecycle-DJpqLXy_.d.mts} +2 -2
  56. package/dist/index.d.mts +12 -1833
  57. package/dist/index.mjs +12 -1357
  58. package/dist/prepared-admission-DRBhl2Sp.mjs +63 -0
  59. package/dist/prepared-admission-DRBhl2Sp.mjs.map +1 -0
  60. package/package.json +1 -1
  61. package/src/{alarm.ts → Alarm.ts} +6 -7
  62. package/src/{browser-rest-capture.ts → BrowserRestCapture.ts} +3 -3
  63. package/src/{browser-rest-crawl.ts → BrowserRestCrawl.ts} +2 -2
  64. package/src/{bindings.ts → CloudflareBindings.ts} +2 -2
  65. package/src/CloudflareBrowser.ts +15 -0
  66. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +3 -3
  67. package/src/{config.ts → CloudflareConfig.ts} +3 -2
  68. package/src/{memory.ts → CloudflareMemory.ts} +15 -12
  69. package/src/{scheduling.ts → CloudflareScheduling.ts} +12 -11
  70. package/src/{subscriptions.ts → CloudflareSubscriptions.ts} +14 -12
  71. package/src/{client.ts → CloudflareThreadClient.ts} +27 -20
  72. package/src/{interactive-browser.ts → InteractiveBrowser.ts} +9 -5
  73. package/src/{thread-object.ts → ThreadObject.ts} +37 -29
  74. package/src/{wake-scheduler.ts → WakeScheduler.ts} +5 -5
  75. package/src/index.ts +11 -30
  76. package/src/{browser-quick-action.ts → internal/browser-quick-action.ts} +10 -8
  77. package/src/{layers.ts → internal/layers.ts} +39 -30
  78. package/src/{prepared-admission.ts → internal/prepared-admission.ts} +7 -7
  79. package/src/{transport.ts → internal/transport.ts} +5 -2
  80. package/src/protected-browser/binding.ts +3 -2
  81. package/src/protected-browser/native.ts +2 -2
  82. package/src/protected-browser/policy.ts +2 -2
  83. package/dist/browser-quick-action.mjs.map +0 -1
  84. package/dist/browser-rest-capture.mjs.map +0 -1
  85. package/dist/browser-rest-crawl.mjs.map +0 -1
  86. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +0 -1
  87. package/dist/index.mjs.map +0 -1
  88. package/dist/interactive-browser.mjs.map +0 -1
  89. package/dist/prepared-admission-BhW2eT_a.mjs +0 -890
  90. package/dist/prepared-admission-BhW2eT_a.mjs.map +0 -1
  91. package/dist/protected-browser.mjs.map +0 -1
  92. package/dist/scheduling.mjs.map +0 -1
  93. package/dist/subscriptions.mjs.map +0 -1
  94. /package/src/{protected-browser.ts → ProtectedBrowser.ts} +0 -0
  95. /package/src/{boundary.ts → internal/boundary.ts} +0 -0
  96. /package/src/{browser-session-lifecycle.ts → internal/browser-session-lifecycle.ts} +0 -0
  97. /package/src/{progress-wait.ts → internal/progress-wait.ts} +0 -0
@@ -1,890 +0,0 @@
1
- import { Clock, Context, Crypto, Duration, Effect, Layer, Option, Predicate, Random, Ref, Schema, Semaphore, Stream } from "effect";
2
- import { DEFAULT_MAX_STORED_VALUE_BYTES } from "@effect-agent/storage-cloudflare";
3
- import { AbortCommand, AbortIntent, AdmissionConflict, AppendConflict, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, CanonicalRecordEnvelope, CanonicalSequence, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeploymentId, DigestError, DurableAgentRuntime, DurableRuntimeFailpointError, FenceRejected, IdempotencyKey, JoinedToHost, LedgerError, OperationDenied, PersistedJson, PreparedInputAdmission, Principal, Receipt, ScheduleStorageError, ScheduledInputAdmission, ScheduledInputRetryable, Settlement, SettlementConflict, SubmissionLedger, ThreadNotMaterialized, ThreadStoreError, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent } from "@effect-agent/thread";
4
- import { AgentId, AgentInputError } from "@effect-agent/core";
5
- import { BrowserCrypto } from "@effect/platform-browser";
6
- import { RpcTracing } from "effect-cf";
7
- //#region src/bindings.ts
8
- /**
9
- * Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
10
- * are supplied as Effect services/Layers"). Application code never reads `env` or touches a
11
- * `DurableObjectState` directly — the Thread Object class constructs these Layers once
12
- * per incarnation and everything downstream consumes the services.
13
- */
14
- /** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */
15
- var CloudflareBindingError = class extends Schema.TaggedError()("CloudflareBindingError", {
16
- binding: Schema.String,
17
- message: Schema.String
18
- }) {};
19
- /**
20
- * The `DurableObjectNamespace` binding that addresses Thread Objects. The Object
21
- * identity rule is `namespace.idFromName(threadId)` (plan §1.2): Thread IDs are
22
- * globally unique, so the mapping is total and deterministic and no directory service exists.
23
- */
24
- var ThreadObjectNamespace = class ThreadObjectNamespace extends Context.Service()("@effect-agent/platform-cloudflare/ThreadObjectNamespace") {
25
- static layer(namespace, options = {}) {
26
- return Layer.succeed(ThreadObjectNamespace)({
27
- namespace,
28
- ...options.rpcTracing === void 0 ? {} : { rpcTracing: options.rpcTracing }
29
- });
30
- }
31
- };
32
- /**
33
- * Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,
34
- * and a namespace binding is a host object no Schema can decode, so this is the documented
35
- * narrowest-boundary check (structural probe for the namespace surface the transport uses);
36
- * a missing or misshaped binding fails typed before any Layer is built.
37
- */
38
- const threadNamespaceFromEnv = Effect.fn("threadNamespaceFromEnv")(function* (env, binding) {
39
- if (!Predicate.isObjectKeyword(env)) return yield* CloudflareBindingError.make({
40
- binding,
41
- message: "The Worker environment is not an object; no bindings are available."
42
- });
43
- const candidate = yield* Effect.try({
44
- try: () => {
45
- const value = Reflect.get(env, binding);
46
- if (!Predicate.isObjectKeyword(value)) return void 0;
47
- const idFromName = Reflect.get(value, "idFromName");
48
- const get = Reflect.get(value, "get");
49
- return typeof idFromName === "function" && typeof get === "function" ? value : void 0;
50
- },
51
- catch: () => CloudflareBindingError.make({
52
- binding,
53
- message: `env.${binding} could not be inspected as a DurableObjectNamespace binding.`
54
- })
55
- });
56
- if (candidate !== void 0) return candidate;
57
- return yield* CloudflareBindingError.make({
58
- binding,
59
- message: `env.${binding} is not a DurableObjectNamespace binding; declare the Thread Object class under this binding in the Worker configuration.`
60
- });
61
- });
62
- /**
63
- * Build the namespace from Worker `env` (fails typed). Enable `rpcTracing` only when the
64
- * receiver also opts into the effect-cf native RPC trace-context contract.
65
- */
66
- const threadNamespaceLayer = (env, binding, options = {}) => Layer.effect(ThreadObjectNamespace)(Effect.map(threadNamespaceFromEnv(env, binding), (namespace) => ({
67
- namespace,
68
- ...options.rpcTracing === true ? { rpcTracing: binding } : {}
69
- })));
70
- /**
71
- * The live Durable Object execution context of THIS incarnation. Only Layer construction and
72
- * the alarm service consume it; important state never lives on it (`ctx.storage` is truth,
73
- * everything in memory is a cache — deployment spec §11).
74
- */
75
- var DurableObjectContext = class DurableObjectContext extends Context.Service()("@effect-agent/platform-cloudflare/DurableObjectContext") {
76
- static layer(ctx, env) {
77
- return Layer.succeed(DurableObjectContext)({
78
- ctx,
79
- env
80
- });
81
- }
82
- };
83
- /**
84
- * The Thread identity this Object serializes and the producer identity its Attempts
85
- * write with (`{producerPrefix}:{threadId}`, plan §1.4). Derived once per incarnation
86
- * from `ctx.id.name` — the Object identity rule guarantees the name IS the Thread ID.
87
- */
88
- var ThreadObjectIdentity = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadObjectIdentity") {};
89
- //#endregion
90
- //#region src/config.ts
91
- /**
92
- * Schema-validated configuration for the Cloudflare durable runtime (deployment spec §4:
93
- * decoded once during Layer construction, exposed as a typed service; DEPLOY-003). Every
94
- * cadence is in milliseconds; every bound is finite and checked before any resource opens.
95
- */
96
- const PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));
97
- const NonNegativeMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
98
- /** The supplied Cloudflare durable runtime configuration failed validation (DEPLOY-003). */
99
- var CloudflarePlatformConfigError = class extends Schema.TaggedError()("CloudflarePlatformConfigError", {
100
- message: Schema.String,
101
- cause: Schema.optionalKey(Schema.Defect())
102
- }) {};
103
- /**
104
- * An admission was refused by a host resource limit BEFORE any ledger row existed
105
- * (deployment spec §8, DEPLOY-007: "Admission has explicit bounded quota and overload
106
- * behavior... a typed rejection"). This is the DC analogue of `NodeDurableHost`'s
107
- * `AdmissionClosed` host gate: the port surface stays untouched, the refusal happens in the
108
- * Thread Object's submit entry point before `DurableAgentRuntime.submit` runs, and
109
- * nothing was admitted or written.
110
- */
111
- var AdmissionLimitExceeded = class extends Schema.TaggedError()("AdmissionLimitExceeded", {
112
- limit: Schema.Literals([
113
- "queue-depth",
114
- "input-bytes",
115
- "database-bytes"
116
- ]),
117
- actual: Schema.Int,
118
- maximum: Schema.Int
119
- }) {
120
- get message() {
121
- return `Admission refused before any ledger row existed: ${this.limit} ${this.actual} exceeds the configured maximum ${this.maximum}. Accepted work is unaffected; retry after the lane drains or raise the limit (DEPLOY-007).`;
122
- }
123
- };
124
- /** The platform's hard per-Object database cap (10 GB, developers.cloudflare.com limits). */
125
- const CLOUDFLARE_DATABASE_CAP_BYTES = 1e10;
126
- /** Default database-size admission ceiling: a 1 GB safety margin under the platform cap. */
127
- const DEFAULT_MAX_DATABASE_BYTES = 9e9;
128
- /**
129
- * Explicit bounded admission quotas checked by the Thread Object BEFORE admission
130
- * (exit gate "resource limits are checked before admission").
131
- */
132
- var CloudflareAdmissionLimitsValue = class extends Schema.Class("@effect-agent/platform-cloudflare/CloudflareAdmissionLimitsValue")({
133
- /** Maximum nonterminal Submissions per Thread lane before new admissions refuse. */
134
- maxQueueDepthPerLane: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1e5)),
135
- /** Maximum encoded input bytes; never above the storage per-value bound. */
136
- maxInputBytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2e6)),
137
- /** Maximum `ctx.storage.sql.databaseSize` at admission; stays under the 10 GB platform cap. */
138
- maxDatabaseBytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(CLOUDFLARE_DATABASE_CAP_BYTES))
139
- }) {};
140
- /**
141
- * Validated Cloudflare durable runtime configuration. The producer identity of one
142
- * Thread Object is `{producerPrefix}:{threadId}` — stable across incarnations of
143
- * the same deployment, distinct across deployments — and producer-epoch fencing (not the
144
- * producer name) remains the correctness authority (DUR-006).
145
- */
146
- var CloudflareDurableRuntimeConfigValue = class extends Schema.Class("@effect-agent/platform-cloudflare/CloudflareDurableRuntimeConfigValue")({
147
- deploymentId: DeploymentId,
148
- /** Head of the minted producer identity `{producerPrefix}:{threadId}`. */
149
- producerPrefix: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
150
- /** Submission ownership lease duration (D5); fences work across Object incarnations. */
151
- ownershipLeaseDuration: PositiveMillis,
152
- /** Base delay of the alarm re-arm backoff when a pass makes no progress. */
153
- alarmBackoffBase: PositiveMillis,
154
- /** Ceiling of the alarm re-arm backoff. */
155
- alarmBackoffCap: PositiveMillis,
156
- /**
157
- * The maintenance-pass scan cadence and the ceiling of every re-arm delay: nonterminal
158
- * work is revisited at least this often (wake/scan pairing, persistence §14).
159
- */
160
- wakeScanInterval: PositiveMillis,
161
- /** `awaitSettlement` ledger re-check cadence when no wake arrives. */
162
- settlementPollInterval: PositiveMillis,
163
- /** Worker ownership-lease renewal cadence during an active Attempt. */
164
- leaseRenewalInterval: PositiveMillis,
165
- /** Active-Run abort-intent poll cadence. */
166
- abortPollInterval: PositiveMillis,
167
- /** Canonical observation poll cadence of the Durable Object store. */
168
- observationPollInterval: NonNegativeMillis,
169
- /** Per-value byte bound; must stay under the platform's 2 MB SQLite value limit. */
170
- maxStoredValueBytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2e6)),
171
- /** Opt-in full payload/digest-chain audit while opening the store. */
172
- verifyOnOpen: Schema.Boolean,
173
- limits: CloudflareAdmissionLimitsValue
174
- }) {};
175
- /** Explicit configuration authority for the assembled Cloudflare durable runtime. */
176
- var CloudflareDurableRuntimeConfig = class extends Context.Service()("@effect-agent/platform-cloudflare/CloudflareDurableRuntimeConfig") {};
177
- /** Documented production defaults applied by `ThreadObject.layer`. */
178
- const CLOUDFLARE_RUNTIME_DEFAULTS = {
179
- ownershipLeaseDuration: Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
180
- alarmBackoffBase: 100,
181
- alarmBackoffCap: 5e3,
182
- wakeScanInterval: 1e3,
183
- settlementPollInterval: 500,
184
- leaseRenewalInterval: 1e4,
185
- abortPollInterval: 500,
186
- observationPollInterval: 25,
187
- maxStoredValueBytes: DEFAULT_MAX_STORED_VALUE_BYTES,
188
- verifyOnOpen: false,
189
- maxQueueDepthPerLane: 256,
190
- maxInputBytes: DEFAULT_MAX_STORED_VALUE_BYTES,
191
- maxDatabaseBytes: DEFAULT_MAX_DATABASE_BYTES
192
- };
193
- //#endregion
194
- //#region src/boundary.ts
195
- const MAX_FOREIGN_DIAGNOSTIC_LENGTH = 8192;
196
- const boundForeignDiagnostic = (message) => message.slice(0, MAX_FOREIGN_DIAGNOSTIC_LENGTH);
197
- /** Render a foreign failure without trusting accessors or coercion hooks on the value. */
198
- const safeCauseMessage = (cause, fallback) => {
199
- try {
200
- const message = cause instanceof Error ? cause.message : cause;
201
- return boundForeignDiagnostic(typeof message === "string" ? message : String(message));
202
- } catch {
203
- return boundForeignDiagnostic(fallback);
204
- }
205
- };
206
- /** Include an Error name when worker-failure classification needs it. */
207
- const safeCauseDiagnostic = (cause, fallback) => {
208
- try {
209
- return cause instanceof Error ? boundForeignDiagnostic(`${cause.name}: ${cause.message}`) : safeCauseMessage(cause, fallback);
210
- } catch {
211
- return boundForeignDiagnostic(fallback);
212
- }
213
- };
214
- /** Read Cloudflare RPC classifications without letting a hostile proxy defect the client. */
215
- const cloudflareFailureSignals = (cause) => {
216
- if (!Predicate.isObjectKeyword(cause)) return {};
217
- try {
218
- const retryableValue = Reflect.get(cause, "retryable");
219
- const overloadedValue = Reflect.get(cause, "overloaded");
220
- const resetValue = Reflect.get(cause, "durableObjectReset");
221
- const retryable = typeof retryableValue === "boolean" ? retryableValue : resetValue === true ? true : void 0;
222
- const overloaded = typeof overloadedValue === "boolean" ? overloadedValue : void 0;
223
- return {
224
- ...retryable === void 0 ? {} : { retryable },
225
- ...overloaded === void 0 ? {} : { overloaded }
226
- };
227
- } catch {
228
- return {};
229
- }
230
- };
231
- //#endregion
232
- //#region src/alarm.ts
233
- /**
234
- * The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
235
- * alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
236
- * and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
237
- * the slot always holds the EARLIEST deadline any caller asked for.
238
- *
239
- * The alarm invariant (plan §1.4): every committed actionable mutation carries a newer durable
240
- * maintenance generation and a committed alarm. Stable externally-driven waits may be
241
- * nonterminal without retaining an alarm; their resolving mutation advances the generation and
242
- * restores the alarm atomically.
243
- */
244
- /** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
245
- var DurableAlarmError = class extends Schema.TaggedError()("DurableAlarmError", {
246
- operation: Schema.String,
247
- message: Schema.String,
248
- cause: Schema.optionalKey(Schema.Defect())
249
- }) {};
250
- const alarmFailure = (operation) => (cause) => DurableAlarmError.make({
251
- operation,
252
- message: safeCauseMessage(cause, "The Cloudflare alarm API failed without a diagnostic"),
253
- cause
254
- });
255
- /** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
256
- var DurableAlarmService = class DurableAlarmService extends Context.Service()("@effect-agent/platform-cloudflare/DurableAlarmService") {
257
- static layer = Layer.effect(DurableAlarmService)(Effect.gen(function* () {
258
- const { ctx } = yield* DurableObjectContext;
259
- /**
260
- * In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
261
- * running pass, and a deferred wake lost to eviction was only ever a promptness hint
262
- * on top of the already-committed pre-armed alarm.
263
- */
264
- const runningPasses = yield* Ref.make(0);
265
- const scheduled = Effect.tryPromise({
266
- try: () => ctx.storage.getAlarm(),
267
- catch: alarmFailure("get alarm")
268
- }).pipe(Effect.map((deadline) => deadline === null ? Option.none() : Option.some(deadline)));
269
- const scheduleAt = (epochMillis) => Effect.tryPromise({
270
- try: () => ctx.storage.setAlarm(epochMillis),
271
- catch: alarmFailure("set alarm")
272
- });
273
- const ensureScheduledBy = (epochMillis) => scheduled.pipe(Effect.flatMap((existing) => Option.isSome(existing) && existing.value <= epochMillis ? Effect.void : scheduleAt(epochMillis)));
274
- const armNow = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => ensureScheduledBy(now)));
275
- const scheduleNow = Ref.get(runningPasses).pipe(Effect.flatMap((passes) => passes > 0 ? Effect.void : armNow));
276
- const withWakesDeferred = (body) => Ref.update(runningPasses, (passes) => passes + 1).pipe(Effect.andThen(body), Effect.ensuring(Ref.update(runningPasses, (passes) => passes - 1)));
277
- const cancel = Effect.tryPromise({
278
- try: () => ctx.storage.deleteAlarm(),
279
- catch: alarmFailure("delete alarm")
280
- });
281
- return DurableAlarmService.of({
282
- scheduled,
283
- scheduleAt,
284
- ensureScheduledBy,
285
- scheduleNow,
286
- withWakesDeferred,
287
- cancel
288
- });
289
- }));
290
- };
291
- /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
292
- var MaintenancePassReport = class extends Schema.Class("@effect-agent/platform-cloudflare/MaintenancePassReport")({
293
- /** `caught-up` is generation-only; `actionable` ran recovery and one bounded drain. */
294
- phase: Schema.Literals(["caught-up", "actionable"]),
295
- /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
296
- recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
297
- /** Settlements the drain pass finalized. */
298
- settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
299
- /** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
300
- nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
301
- /** `rearmed` for dirty/autonomous work, `cleared` for stable waits or settlement. */
302
- alarm: Schema.Literals(["rearmed", "cleared"])
303
- }) {};
304
- /** Test-only fault authority; production uses the inert layer. */
305
- var ThreadMaintenanceFailpoint = class extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenanceFailpoint") {
306
- static layer = Layer.succeed(this)({ hit: () => Effect.void });
307
- };
308
- const MaintenanceGeneration = Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n));
309
- /** Versioned, platform-private maintenance state stored through Durable Object KV. */
310
- var ThreadMaintenanceState = class extends Schema.Class("@effect-agent/platform-cloudflare/ThreadMaintenanceState")({
311
- schemaVersion: Schema.Literal(1),
312
- dirty: MaintenanceGeneration,
313
- processed: MaintenanceGeneration,
314
- nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
315
- }) {};
316
- const MAINTENANCE_STATE_KEY = "effect-agent:thread-maintenance:v1";
317
- const decodeMaintenanceState = Schema.decodeUnknownSync(ThreadMaintenanceState);
318
- const encodeMaintenanceState = Schema.encodeSync(ThreadMaintenanceState);
319
- const initialMaintenanceState = () => ThreadMaintenanceState.make({
320
- schemaVersion: 1,
321
- dirty: 1n,
322
- processed: 0n,
323
- nonterminal: 0
324
- });
325
- const readMaintenanceState = async (transaction) => {
326
- const encoded = await transaction.get(MAINTENANCE_STATE_KEY);
327
- return encoded === void 0 ? {
328
- state: initialMaintenanceState(),
329
- initialized: false
330
- } : {
331
- state: decodeMaintenanceState(encoded),
332
- initialized: true
333
- };
334
- };
335
- const ensureTransactionAlarmBy = async (transaction, deadline) => {
336
- const scheduled = await transaction.getAlarm();
337
- if (scheduled === null || scheduled > deadline) await transaction.setAlarm(deadline);
338
- };
339
- const stableExternalWait = (snapshot, reports) => {
340
- const decision = reports.get(snapshot.submissionId)?.decision._tag;
341
- if (decision === "SettleAborted") return false;
342
- switch (snapshot.state) {
343
- case "suspended":
344
- case "joined": return true;
345
- case "unknown": return decision === "AwaitUnknownResolution" || decision === "MarkUnknown";
346
- case "admitted": return reports.get(snapshot.submissionId)?.decision._tag === "AwaitParentEstablishment";
347
- case "input-applied":
348
- case "joining":
349
- case "ready":
350
- case "running":
351
- case "settled":
352
- case "terminalizing": return false;
353
- }
354
- };
355
- /**
356
- * Incremental, quiescent maintenance over a durable dirty/processed generation (issue #93).
357
- *
358
- * `pass` = generation snapshot/pre-arm → recovery → bounded drain → generation acknowledgement:
359
- *
360
- * 1. One storage transaction reads dirty/processed and re-arms before work. A caught-up forced
361
- * alarm takes an O(1) path without recovery, ledger scans, or canonical-history reads.
362
- * 2. Recovery still strictly precedes a new claim, and one bounded drain advances the lane.
363
- * 3. The final transaction acknowledges only the generation observed at pass start. A racing
364
- * mutation therefore remains `dirty > processed` and retains its atomically-established alarm.
365
- * 4. Stable external waits acknowledge and clear. Autonomous retry, indeterminate, and lease
366
- * recovery states leave their generation dirty and retain bounded backoff rearming.
367
- */
368
- var ThreadMaintenance = class ThreadMaintenance extends Context.Service()("@effect-agent/platform-cloudflare/ThreadMaintenance") {
369
- static layer = Layer.effect(ThreadMaintenance)(Effect.gen(function* () {
370
- const runtime = yield* DurableAgentRuntime;
371
- const ledger = yield* SubmissionLedger;
372
- const alarm = yield* DurableAlarmService;
373
- const config = yield* CloudflareDurableRuntimeConfig;
374
- const identity = yield* ThreadObjectIdentity;
375
- const { ctx } = yield* DurableObjectContext;
376
- const failpoint = yield* ThreadMaintenanceFailpoint;
377
- /**
378
- * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
379
- * restarts at zero and merely re-arms sooner than a long-lived one would have.
380
- */
381
- const stalls = yield* Ref.make(0);
382
- /**
383
- * Incarnation-local mutation count guarded with the generation transactions below. It is
384
- * deliberately not durable: after eviction every begun mutation has stopped, while its
385
- * pre-armed dirty generation remains durable for recovery. The short gate never spans the
386
- * caller's mutation or cross-Object I/O.
387
- */
388
- const activeMutations = yield* Ref.make(0);
389
- const generationGate = yield* Semaphore.make(1);
390
- const maintenancePassGate = yield* Semaphore.make(1);
391
- const minimumAlarmDelay = Math.max(1, Math.ceil(config.alarmBackoffBase / 2));
392
- const runTransaction = (operation, transaction) => Effect.tryPromise({
393
- try: transaction,
394
- catch: alarmFailure(operation)
395
- });
396
- const beginMutation = Effect.fn("ThreadMaintenance.beginMutation")(function* () {
397
- yield* failpoint.hit("maintenance:dirty:before");
398
- const now = yield* Clock.currentTimeMillis;
399
- yield* runTransaction("advance maintenance generation", () => ctx.storage.transaction(async (transaction) => {
400
- const { state } = await readMaintenanceState(transaction);
401
- const next = ThreadMaintenanceState.make({
402
- ...state,
403
- dirty: state.dirty + 1n
404
- });
405
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
406
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
407
- }));
408
- yield* failpoint.hit("maintenance:dirty:after");
409
- yield* Ref.update(activeMutations, (active) => active + 1);
410
- });
411
- const endMutation = generationGate.withPermit(Ref.update(activeMutations, (active) => Math.max(0, active - 1)));
412
- 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);
413
- const ensureAlarm = Effect.fn("ThreadMaintenance.ensureAlarm")(function* () {
414
- yield* failpoint.hit("maintenance:ensure:before");
415
- const now = yield* Clock.currentTimeMillis;
416
- yield* runTransaction("ensure maintenance alarm", () => ctx.storage.transaction(async (transaction) => {
417
- const { state, initialized } = await readMaintenanceState(transaction);
418
- if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
419
- if (state.dirty > state.processed) await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
420
- }));
421
- yield* failpoint.hit("maintenance:ensure:after");
422
- });
423
- const beginPass = Effect.fn("ThreadMaintenance.beginPass")(function* () {
424
- yield* failpoint.hit("maintenance:begin:before");
425
- const now = yield* Clock.currentTimeMillis;
426
- const result = yield* runTransaction("begin maintenance pass", () => ctx.storage.transaction(async (transaction) => {
427
- const { state, initialized } = await readMaintenanceState(transaction);
428
- if (!initialized) await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(state));
429
- if (state.processed >= state.dirty) {
430
- await transaction.deleteAlarm();
431
- return {
432
- _tag: "CaughtUp",
433
- nonterminal: state.nonterminal
434
- };
435
- }
436
- await ensureTransactionAlarmBy(transaction, now + minimumAlarmDelay);
437
- return {
438
- _tag: "Actionable",
439
- generation: state.dirty
440
- };
441
- }));
442
- yield* failpoint.hit("maintenance:begin:after");
443
- return result;
444
- });
445
- const rearmDelay = Effect.fn("ThreadMaintenance.rearmDelay")(function* (progressed) {
446
- const priorStalls = yield* Ref.getAndUpdate(stalls, (count) => progressed ? 0 : count + 1);
447
- if (progressed) return config.alarmBackoffBase;
448
- const exponent = Math.min(priorStalls, 30);
449
- const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
450
- const jitter = yield* Random.next;
451
- const jittered = Math.ceil(backoff / 2 + backoff / 2 * jitter);
452
- return Math.min(jittered, config.wakeScanInterval);
453
- });
454
- const pass = Effect.fn("ThreadMaintenance.pass")(function* () {
455
- const annotate = (report) => Effect.annotateCurrentSpan({
456
- phase: report.phase,
457
- recovered: report.recovered,
458
- settled: report.settled,
459
- nonterminal: report.nonterminal,
460
- alarm: report.alarm
461
- }).pipe(Effect.as(report));
462
- const started = yield* generationGate.withPermit(Effect.gen(function* () {
463
- const activeAtStart = yield* Ref.get(activeMutations);
464
- return {
465
- ...yield* beginPass(),
466
- activeAtStart
467
- };
468
- }));
469
- if (started._tag === "CaughtUp") return yield* annotate(MaintenancePassReport.make({
470
- phase: "caught-up",
471
- recovered: 0,
472
- settled: 0,
473
- nonterminal: started.nonterminal,
474
- alarm: "cleared"
475
- }));
476
- const recovered = yield* runtime.runRecovery;
477
- const settlements = yield* runtime.processThreadResolved(identity.threadId);
478
- const remaining = yield* Stream.runCollect(ledger.scanNonterminal);
479
- const reports = new Map(recovered.map((report) => [report.submissionId, report]));
480
- const head = remaining[0];
481
- const headWaiting = head !== void 0 && stableExternalWait(head, reports);
482
- const autonomous = remaining.some((snapshot, index) => {
483
- if (index > 0 && headWaiting && snapshot.state === "ready" && reports.get(snapshot.submissionId)?.decision._tag === "ApplyInput") return false;
484
- return !stableExternalWait(snapshot, reports);
485
- });
486
- const progressed = settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
487
- const delay = autonomous ? yield* rearmDelay(progressed) : 0;
488
- const now = yield* Clock.currentTimeMillis;
489
- yield* failpoint.hit("maintenance:finish:before");
490
- const alarmDisposition = yield* generationGate.withPermit(Effect.gen(function* () {
491
- const active = yield* Ref.get(activeMutations);
492
- return yield* runTransaction("finish maintenance pass", () => ctx.storage.transaction(async (transaction) => {
493
- const { state } = await readMaintenanceState(transaction);
494
- const processed = autonomous || started.activeAtStart > 0 || active > 0 ? state.processed : state.processed > started.generation ? state.processed : started.generation;
495
- const next = ThreadMaintenanceState.make({
496
- ...state,
497
- processed,
498
- nonterminal: remaining.length
499
- });
500
- await transaction.put(MAINTENANCE_STATE_KEY, encodeMaintenanceState(next));
501
- if (autonomous) {
502
- await transaction.setAlarm(now + delay);
503
- return "rearmed";
504
- }
505
- if (started.activeAtStart > 0 || active > 0 || next.dirty > next.processed) {
506
- await ensureTransactionAlarmBy(transaction, now + config.wakeScanInterval);
507
- return "rearmed";
508
- }
509
- await transaction.deleteAlarm();
510
- return "cleared";
511
- }));
512
- }));
513
- yield* failpoint.hit("maintenance:finish:after");
514
- if (alarmDisposition === "cleared") yield* Ref.set(stalls, 0);
515
- return yield* annotate(MaintenancePassReport.make({
516
- phase: "actionable",
517
- recovered: recovered.length,
518
- settled: settlements.length,
519
- nonterminal: remaining.length,
520
- alarm: alarmDisposition
521
- }));
522
- });
523
- return ThreadMaintenance.of({
524
- pass: alarm.withWakesDeferred(maintenancePassGate.withPermit(pass())),
525
- ensureAlarm: ensureAlarm(),
526
- withMutation
527
- });
528
- }));
529
- };
530
- //#endregion
531
- //#region src/client.ts
532
- /**
533
- * The Worker↔Thread-Object host protocol (plan §1.4): Schema envelopes for the host
534
- * entry points (`submitEncoded`, `awaitSettlementEncoded`, `observePage`, `abortEncoded`,
535
- * `resolveApprovalEncoded`, `resolveUnknownEncoded`) plus the Worker-side client that speaks
536
- * it. Mirrors the WP2 port protocol: requests and responses are closed Schema unions, typed
537
- * failures travel as their own tagged classes and RE-DECODE to identical tags on the caller
538
- * (error-tag fidelity), and protocol anomalies are answered typed instead of thrown.
539
- */
540
- /** Ceiling for host protocol diagnostic strings. */
541
- const MAX_HOST_DIAGNOSTIC_LENGTH = 4096;
542
- const BoundedDiagnostic = Schema.String.check(Schema.isMaxLength(MAX_HOST_DIAGNOSTIC_LENGTH));
543
- /** Truncate a diagnostic string to the host protocol's bounded length. */
544
- const boundHostDiagnostic = (value) => value.length > MAX_HOST_DIAGNOSTIC_LENGTH ? `${value.slice(0, 4093)}...` : value;
545
- /**
546
- * The envelope itself could not be honored: the Object could not decode the request, or a
547
- * response could not be encoded/decoded. Never carries operation semantics.
548
- */
549
- var HostProtocolError = class extends Schema.TaggedError()("HostProtocolError", { message: BoundedDiagnostic }) {};
550
- /** The Worker-side stub call itself failed (RPC rejection, overload, eviction mid-call). */
551
- var ThreadClientError = class extends Schema.TaggedError()("ThreadClientError", {
552
- threadId: Schema.String,
553
- message: Schema.String,
554
- cause: Schema.optionalKey(Schema.Defect()),
555
- /** Cloudflare's own classification for a failure safe to retry with a fresh stub. */
556
- retryable: Schema.optionalKey(Schema.Boolean),
557
- /** Cloudflare overloads are surfaced immediately instead of adding retry pressure. */
558
- overloaded: Schema.optionalKey(Schema.Boolean)
559
- }) {};
560
- /**
561
- * One durable submission, input ALREADY encoded by the caller through the Agent Binding's
562
- * input schema (the Worker bundles the same Agent definitions as the Object, so schema
563
- * validation happens client-side; the resolved Binding re-validates at claim time). The
564
- * Thread identity is deliberately absent — the addressed Object IS the lane.
565
- */
566
- var SubmitRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/SubmitRequest")({
567
- agentId: AgentId,
568
- principal: Principal,
569
- idempotencyKey: IdempotencyKey,
570
- definitions: DefinitionDigests,
571
- inputPayload: PersistedJson
572
- }) {};
573
- /** One bounded page of canonical records after an optional sequence. */
574
- var ObservePageRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/ObservePageRequest")({
575
- afterSequence: Schema.optionalKey(CanonicalSequence),
576
- limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
577
- }) {};
578
- /** One event-driven wait for canonical progress strictly after this sequence. */
579
- var AwaitProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/AwaitProgressRequest")({
580
- afterSequence: CanonicalSequence,
581
- waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256))
582
- }) {};
583
- /** Best-effort cancellation of one in-flight progress RPC. */
584
- var CancelProgressRequest = class extends Schema.Class("@effect-agent/platform-cloudflare/CancelProgressRequest")({ waiterId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256)) }) {};
585
- /**
586
- * Every typed failure a host entry point can produce, plus the protocol's own errors. Same
587
- * closed-union discipline as the WP2 `PortFailure`: members re-decode to the SAME tagged
588
- * classes on the Worker side; `cause` chains travel as Schema defects without instance
589
- * fidelity (plan §2.8).
590
- */
591
- const HostFailure = Schema.Union([
592
- AgentInputError,
593
- DigestError,
594
- AdmissionConflict,
595
- SettlementConflict,
596
- ApprovalConflict,
597
- UnknownResolutionConflict,
598
- JoinedToHost,
599
- LedgerError,
600
- ThreadStoreError,
601
- ThreadNotMaterialized,
602
- AppendConflict,
603
- FenceRejected,
604
- DurableRuntimeFailpointError,
605
- AdmissionLimitExceeded,
606
- DurableAlarmError,
607
- OperationDenied,
608
- HostProtocolError
609
- ]);
610
- var SubmitSucceeded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SubmitSucceeded")("SubmitSucceeded", { receipt: Receipt }) {};
611
- var SettlementReached = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/SettlementReached")("SettlementReached", { settlement: Settlement }) {};
612
- var ObservedPage = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ObservedPage")("ObservedPage", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
613
- /** A record was already committed or an incarnation-local hint says the caller should re-read. */
614
- var ProgressObserved = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressObserved")("ProgressObserved", {}) {};
615
- var ProgressCancelled = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ProgressCancelled")("ProgressCancelled", {}) {};
616
- var AbortRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/AbortRecorded")("AbortRecorded", { intent: AbortIntent }) {};
617
- var ApprovalRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/ApprovalRecorded")("ApprovalRecorded", { intent: ApprovalDecisionIntent }) {};
618
- var UnknownResolutionRecorded = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/UnknownResolutionRecorded")("UnknownResolutionRecorded", { intent: UnknownResolutionIntent }) {};
619
- /** The entry point failed TYPED on the Object; the failure re-decodes verbatim. */
620
- var HostFailed = class extends Schema.TaggedClass("@effect-agent/platform-cloudflare/HostFailed")("HostFailed", { failure: HostFailure }) {};
621
- /** The uniform answer of one host entry point. Callers narrow by the tag their call implies. */
622
- const HostResponse = Schema.Union([
623
- SubmitSucceeded,
624
- SettlementReached,
625
- ObservedPage,
626
- ProgressObserved,
627
- ProgressCancelled,
628
- AbortRecorded,
629
- ApprovalRecorded,
630
- UnknownResolutionRecorded,
631
- HostFailed
632
- ]);
633
- const decodeSubmitRequest = Schema.decodeUnknownEffect(SubmitRequest);
634
- const encodeSubmitRequest = Schema.encodeEffect(SubmitRequest);
635
- const decodeReceipt = Schema.decodeUnknownEffect(Receipt);
636
- const encodeReceipt = Schema.encodeEffect(Receipt);
637
- const decodeObservePageRequest = Schema.decodeUnknownEffect(ObservePageRequest);
638
- const encodeObservePageRequest = Schema.encodeEffect(ObservePageRequest);
639
- const decodeAwaitProgressRequest = Schema.decodeUnknownEffect(AwaitProgressRequest);
640
- const encodeAwaitProgressRequest = Schema.encodeEffect(AwaitProgressRequest);
641
- const decodeCancelProgressRequest = Schema.decodeUnknownEffect(CancelProgressRequest);
642
- const encodeCancelProgressRequest = Schema.encodeEffect(CancelProgressRequest);
643
- const decodeAbortCommand = Schema.decodeUnknownEffect(AbortCommand);
644
- const encodeAbortCommand = Schema.encodeEffect(AbortCommand);
645
- const decodeApprovalDecisionCommand = Schema.decodeUnknownEffect(ApprovalDecisionCommand);
646
- const encodeApprovalDecisionCommand = Schema.encodeEffect(ApprovalDecisionCommand);
647
- const decodeUnknownResolutionCommand = Schema.decodeUnknownEffect(UnknownResolutionCommand);
648
- const encodeUnknownResolutionCommand = Schema.encodeEffect(UnknownResolutionCommand);
649
- const encodeHostResponse = Schema.encodeEffect(HostResponse);
650
- const decodeHostResponse = Schema.decodeUnknownEffect(HostResponse);
651
- /** Failure surface of `CloudflareThreadClient.submit`. */
652
- const ClientSubmitHostFailure = Schema.Union([
653
- AgentInputError,
654
- DigestError,
655
- AdmissionConflict,
656
- LedgerError,
657
- ThreadStoreError,
658
- ThreadNotMaterialized,
659
- AppendConflict,
660
- FenceRejected,
661
- DurableRuntimeFailpointError,
662
- AdmissionLimitExceeded,
663
- DurableAlarmError,
664
- HostProtocolError
665
- ]);
666
- const ClientAwaitHostFailure = Schema.Union([
667
- LedgerError,
668
- SettlementConflict,
669
- OperationDenied,
670
- HostProtocolError
671
- ]);
672
- const ClientObserveHostFailure = Schema.Union([
673
- ThreadStoreError,
674
- ThreadNotMaterialized,
675
- OperationDenied,
676
- HostProtocolError
677
- ]);
678
- const ClientAbortHostFailure = Schema.Union([
679
- OperationDenied,
680
- LedgerError,
681
- SettlementConflict,
682
- JoinedToHost,
683
- DurableRuntimeFailpointError,
684
- DurableAlarmError,
685
- HostProtocolError
686
- ]);
687
- const ClientApprovalHostFailure = Schema.Union([
688
- LedgerError,
689
- SettlementConflict,
690
- ApprovalConflict,
691
- OperationDenied,
692
- DurableAlarmError,
693
- HostProtocolError
694
- ]);
695
- const ClientUnknownHostFailure = Schema.Union([
696
- LedgerError,
697
- SettlementConflict,
698
- UnknownResolutionConflict,
699
- JoinedToHost,
700
- DurableRuntimeFailpointError,
701
- OperationDenied,
702
- DurableAlarmError,
703
- HostProtocolError
704
- ]);
705
- const outOfContract = (threadId, operation, observed) => ThreadClientError.make({
706
- threadId,
707
- message: boundHostDiagnostic(`The Thread Object answered ${operation} with the out-of-contract ${observed}.`)
708
- });
709
- const hostRpcMethods = {
710
- submit: "submitEncoded",
711
- awaitSettlement: "awaitSettlementEncoded",
712
- awaitProgress: "awaitProgressEncoded",
713
- cancelProgress: "cancelProgressEncoded",
714
- observePage: "observePage",
715
- abort: "abortEncoded",
716
- resolveApproval: "resolveApprovalEncoded",
717
- resolveUnknown: "resolveUnknownEncoded"
718
- };
719
- /** Worker-side client over the Thread Object namespace (DEPLOY-010). */
720
- var CloudflareThreadClient = class CloudflareThreadClient extends Context.Service()("@effect-agent/platform-cloudflare/CloudflareThreadClient") {
721
- /**
722
- * Assemble the client from a resolved namespace and the platform Crypto implementation.
723
- * rpcTracing is the binding name and remains opt-in. Caller authentication, principals,
724
- * idempotency keys, and definition digests are still explicit submission inputs.
725
- */
726
- static layerFromBinding(options) {
727
- return CloudflareThreadClient.layer.pipe(Layer.provide([ThreadObjectNamespace.layer(options.namespace, options), BrowserCrypto.layer]));
728
- }
729
- static layer = Layer.effect(CloudflareThreadClient)(Effect.gen(function* () {
730
- const { namespace, rpcTracing } = yield* ThreadObjectNamespace;
731
- const crypto = yield* Crypto.Crypto;
732
- const call = Effect.fn(function* (threadId, operation, encoded) {
733
- const traceArgs = rpcTracing === void 0 ? [] : yield* RpcTracing.withRpcTraceContext([]);
734
- const raw = yield* Effect.tryPromise({
735
- try: () => {
736
- return namespace.get(namespace.idFromName(threadId))[hostRpcMethods[operation]](encoded, ...traceArgs);
737
- },
738
- catch: (cause) => ThreadClientError.make({
739
- threadId,
740
- message: boundHostDiagnostic(`${operation} did not reach the Thread Object: ${safeCauseMessage(cause, "the RPC failed without a diagnostic")}`),
741
- cause,
742
- ...cloudflareFailureSignals(cause)
743
- })
744
- });
745
- return yield* decodeHostResponse(raw).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`The ${operation} answer could not be decoded: ${error.message}`) })));
746
- }, (effect, threadId, operation) => rpcTracing === void 0 ? Effect.withSpan(effect, "CloudflareThreadClient.call", { attributes: {
747
- threadId,
748
- operation
749
- } }) : RpcTracing.withRpcClientSpan(effect, rpcTracing, hostRpcMethods[operation]));
750
- const expect = (threadId, operation, resultSchema, failureSchema) => {
751
- const isExpectedResult = Schema.is(resultSchema);
752
- const isExpectedFailure = Schema.is(failureSchema);
753
- return (response) => {
754
- if (response._tag === "HostFailed") {
755
- const failure = response.failure;
756
- return isExpectedFailure(failure) ? Effect.fail(failure) : Effect.fail(outOfContract(threadId, operation, `failure ${failure._tag}`));
757
- }
758
- if (!isExpectedResult(response)) return Effect.fail(outOfContract(threadId, operation, `result ${response._tag}`));
759
- return Effect.succeed(response);
760
- };
761
- };
762
- const readPage = (threadId, options) => Effect.gen(function* () {
763
- const request = ObservePageRequest.make({
764
- ...options?.afterSequence === void 0 ? {} : { afterSequence: options.afterSequence },
765
- limit: options?.limit ?? 256
766
- });
767
- const encoded = yield* encodeObservePageRequest(request).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`observePage request encode failed: ${error.message}`) })));
768
- const response = yield* call(threadId, "observePage", encoded);
769
- return (yield* expect(threadId, "observePage", ObservedPage, ClientObserveHostFailure)(response)).records;
770
- });
771
- const cancelProgress = (threadId, waiterId) => encodeCancelProgressRequest(CancelProgressRequest.make({ waiterId })).pipe(Effect.mapError(() => void 0), Effect.flatMap((encoded) => call(threadId, "cancelProgress", encoded)), Effect.asVoid, Effect.ignore);
772
- return CloudflareThreadClient.of({
773
- submit: (agent, input, options) => Effect.gen(function* () {
774
- const encodedInput = yield* Schema.encodeEffect(agent.definition.input)(input).pipe(Effect.mapError((cause) => AgentInputError.make({ message: `Unable to encode Agent input: ${cause.message}` })));
775
- const inputPayload = yield* Schema.decodeUnknownEffect(PersistedJson)(encodedInput).pipe(Effect.mapError(() => AgentInputError.make({ message: "Agent input does not satisfy the canonical persistence bounds" })));
776
- const request = SubmitRequest.make({
777
- agentId: agent.definition.id,
778
- principal: options.principal,
779
- idempotencyKey: options.idempotencyKey,
780
- definitions: options.definitions,
781
- inputPayload
782
- });
783
- const encoded = yield* encodeSubmitRequest(request).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`submit request encode failed: ${error.message}`) })));
784
- const response = yield* call(options.threadId, "submit", encoded);
785
- return (yield* expect(options.threadId, "submit", SubmitSucceeded, ClientSubmitHostFailure)(response)).receipt;
786
- }),
787
- awaitSettlement: (receipt) => Effect.gen(function* () {
788
- const encoded = yield* encodeReceipt(receipt).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`receipt encode failed: ${error.message}`) })));
789
- const response = yield* call(receipt.threadId, "awaitSettlement", encoded);
790
- return (yield* expect(receipt.threadId, "awaitSettlement", SettlementReached, ClientAwaitHostFailure)(response)).settlement;
791
- }),
792
- awaitProgress: (threadId, afterSequence) => Effect.gen(function* () {
793
- const waiterId = yield* crypto.randomUUIDv4.pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress cancellation identity generation failed: ${error.message}`) })));
794
- const request = AwaitProgressRequest.make({
795
- afterSequence,
796
- waiterId
797
- });
798
- const encoded = yield* encodeAwaitProgressRequest(request).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`awaitProgress request encode failed: ${error.message}`) })));
799
- const attempt = (retry) => call(threadId, "awaitProgress", encoded).pipe(Effect.flatMap(expect(threadId, "awaitProgress", ProgressObserved, ClientObserveHostFailure)), Effect.asVoid, Effect.catchTag("ThreadClientError", (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)));
800
- yield* attempt(0).pipe(Effect.onInterrupt(() => cancelProgress(threadId, waiterId)));
801
- }),
802
- readPage,
803
- readAll: (threadId) => Effect.gen(function* () {
804
- const all = [];
805
- let after;
806
- for (;;) {
807
- const page = yield* readPage(threadId, {
808
- afterSequence: after,
809
- limit: 1024
810
- });
811
- all.push(...page);
812
- const last = page.at(-1);
813
- if (page.length < 1024 || last === void 0) return all;
814
- after = last.sequence;
815
- }
816
- }),
817
- abort: (threadId, command) => Effect.gen(function* () {
818
- const encoded = yield* encodeAbortCommand(command).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`abort command encode failed: ${error.message}`) })));
819
- const response = yield* call(threadId, "abort", encoded);
820
- return (yield* expect(threadId, "abort", AbortRecorded, ClientAbortHostFailure)(response)).intent;
821
- }),
822
- resolveApproval: (threadId, command) => Effect.gen(function* () {
823
- const encoded = yield* encodeApprovalDecisionCommand(command).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`approval command encode failed: ${error.message}`) })));
824
- const response = yield* call(threadId, "resolveApproval", encoded);
825
- return (yield* expect(threadId, "resolveApproval", ApprovalRecorded, ClientApprovalHostFailure)(response)).intent;
826
- }),
827
- resolveUnknown: (threadId, command) => Effect.gen(function* () {
828
- const encoded = yield* encodeUnknownResolutionCommand(command).pipe(Effect.mapError((error) => HostProtocolError.make({ message: boundHostDiagnostic(`resolution command encode failed: ${error.message}`) })));
829
- const response = yield* call(threadId, "resolveUnknown", encoded);
830
- return (yield* expect(threadId, "resolveUnknown", UnknownResolutionRecorded, ClientUnknownHostFailure)(response)).intent;
831
- })
832
- });
833
- }));
834
- };
835
- //#endregion
836
- //#region src/prepared-admission.ts
837
- const passthroughAgent = (agentId) => ({ definition: {
838
- id: agentId,
839
- input: PersistedJson
840
- } });
841
- /** Ordinary prepared admission through one freshly addressed Thread Object call. */
842
- const cloudflarePreparedInputAdmissionLayer = Layer.effect(PreparedInputAdmission, Effect.gen(function* () {
843
- const client = yield* CloudflareThreadClient;
844
- return PreparedInputAdmission.of({ submit: (envelope) => client.submit(passthroughAgent(envelope.agentId), envelope.input, {
845
- threadId: envelope.threadId,
846
- principal: envelope.deliveryPrincipal,
847
- idempotencyKey: envelope.admissionKey,
848
- definitions: envelope.definitions
849
- }).pipe(Effect.catchTags({
850
- AdmissionConflict: () => ScheduleStorageError.make({
851
- operation: "prepared admission",
852
- reason: "corrupt"
853
- }),
854
- AdmissionLimitExceeded: () => ScheduledInputRetryable.make({ reason: "capacity" }),
855
- ThreadClientError: (error) => ScheduledInputRetryable.make({ reason: error.overloaded === true ? "capacity" : "transport" }),
856
- HostProtocolError: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
857
- LedgerError: () => ScheduledInputRetryable.make({ reason: "storage" }),
858
- ThreadStoreError: () => ScheduledInputRetryable.make({ reason: "storage" }),
859
- DurableAlarmError: () => ScheduledInputRetryable.make({ reason: "storage" }),
860
- AgentInputError: () => ScheduleStorageError.make({
861
- operation: "prepared admission",
862
- reason: "corrupt"
863
- }),
864
- DigestError: () => ScheduleStorageError.make({
865
- operation: "prepared admission",
866
- reason: "corrupt"
867
- }),
868
- ThreadNotMaterialized: () => ScheduledInputRetryable.make({ reason: "storage" }),
869
- AppendConflict: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
870
- FenceRejected: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
871
- DurableRuntimeFailpointError: () => ScheduledInputRetryable.make({ reason: "ambiguous" })
872
- })) });
873
- }));
874
- const preparedFromSchedule = (envelope) => ({
875
- schemaVersion: 1,
876
- threadId: envelope.threadId,
877
- deliveryPrincipal: envelope.deliveryPrincipal,
878
- agentId: envelope.agentId,
879
- definitions: envelope.definitions,
880
- input: envelope.input,
881
- inputDigest: envelope.inputDigest,
882
- admissionKey: envelope.admissionKey,
883
- authorization: envelope.authorization
884
- });
885
- /** Compatibility adapter retaining Scheduling's public admission port. */
886
- const cloudflareScheduledInputAdmissionLayer = Layer.effect(ScheduledInputAdmission, Effect.map(PreparedInputAdmission, (admission) => ScheduledInputAdmission.of({ submit: (envelope) => admission.submit(preparedFromSchedule(envelope)) })));
887
- //#endregion
888
- export { CloudflarePlatformConfigError as $, decodeUnknownResolutionCommand as A, DurableAlarmError as B, decodeApprovalDecisionCommand as C, decodeObservePageRequest as D, decodeHostResponse as E, encodeHostResponse as F, safeCauseDiagnostic as G, MaintenancePassReport as H, encodeObservePageRequest as I, CLOUDFLARE_DATABASE_CAP_BYTES as J, safeCauseMessage as K, encodeReceipt as L, encodeApprovalDecisionCommand as M, encodeAwaitProgressRequest as N, decodeReceipt as O, encodeCancelProgressRequest as P, CloudflareDurableRuntimeConfigValue as Q, encodeSubmitRequest as R, decodeAbortCommand as S, decodeCancelProgressRequest as T, ThreadMaintenance as U, DurableAlarmService as V, ThreadMaintenanceFailpoint as W, CloudflareAdmissionLimitsValue as X, CLOUDFLARE_RUNTIME_DEFAULTS as Y, CloudflareDurableRuntimeConfig as Z, SubmitRequest as _, AwaitProgressRequest as a, threadNamespaceFromEnv as at, UnknownResolutionRecorded as b, HostFailed as c, HostResponse as d, DEFAULT_MAX_DATABASE_BYTES as et, ObservePageRequest as f, SettlementReached as g, ProgressObserved as h, ApprovalRecorded as i, ThreadObjectNamespace as it, encodeAbortCommand as j, decodeSubmitRequest as k, HostFailure as l, ProgressCancelled as m, cloudflareScheduledInputAdmissionLayer as n, DurableObjectContext as nt, CancelProgressRequest as o, threadNamespaceLayer as ot, ObservedPage as p, AdmissionLimitExceeded as q, AbortRecorded as r, ThreadObjectIdentity as rt, CloudflareThreadClient as s, cloudflarePreparedInputAdmissionLayer as t, CloudflareBindingError as tt, HostProtocolError as u, SubmitSucceeded as v, decodeAwaitProgressRequest as w, boundHostDiagnostic as x, ThreadClientError as y, encodeUnknownResolutionCommand as z };
889
-
890
- //# sourceMappingURL=prepared-admission-BhW2eT_a.mjs.map