@effect-agent/platform-cloudflare 0.1.0-beta.9 → 0.1.0-beta.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Alarm.d.mts +188 -0
- package/dist/Alarm.mjs +451 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1496 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-DUE_m12-.d.mts +161 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs +1169 -0
- package/dist/InteractiveBrowser-DupDtfw2.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ProtectedBrowser.d.mts +123 -0
- package/dist/ProtectedBrowser.mjs +1135 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-CIbmXZCY.d.mts +812 -0
- package/dist/ThreadObject-DvRG0dDz.mjs +835 -0
- package/dist/ThreadObject-DvRG0dDz.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +57 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/index.d.mts +13 -1548
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +973 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +2354 -0
- package/src/ProtectedBrowser.ts +9 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +12 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +751 -0
- package/src/internal/message-delivery.ts +152 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/src/protected-browser/binding.ts +313 -0
- package/src/protected-browser/host.ts +344 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +445 -0
- package/src/protected-browser/policy.ts +821 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
package/src/alarm.ts
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AgentBindingResolver,
|
|
3
|
-
DurableAgentRuntime,
|
|
4
|
-
SubmissionLedger,
|
|
5
|
-
type DurableBindingFailure,
|
|
6
|
-
type DurableWorkerFailure,
|
|
7
|
-
type RecoveryReport,
|
|
8
|
-
} from "@effect-agent/session";
|
|
9
|
-
import { Clock, Context, Effect, Layer, Option, Random, Ref, Schema, Stream } from "effect";
|
|
10
|
-
|
|
11
|
-
import { ConversationObjectIdentity, DurableObjectContext } from "./bindings.ts";
|
|
12
|
-
import { CloudflareDurableRuntimeConfig } from "./config.ts";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
|
|
16
|
-
* alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
|
|
17
|
-
* and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
|
|
18
|
-
* the slot always holds the EARLIEST deadline any caller asked for.
|
|
19
|
-
*
|
|
20
|
-
* The alarm invariant (plan §1.4): committed nonterminal work implies a committed alarm.
|
|
21
|
-
* It is established by pre-arming — every mutating entry point and every pass arms the alarm
|
|
22
|
-
* BEFORE its durable mutations — so an eviction at any failpoint leaves a persisted alarm
|
|
23
|
-
* that workerd re-delivers to a fresh incarnation WITHOUT any incoming request. Spurious
|
|
24
|
-
* alarms are harmless by design: a pass over an all-settled lane simply deletes the slot.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
|
|
28
|
-
export class DurableAlarmError extends Schema.TaggedError<DurableAlarmError>()(
|
|
29
|
-
"DurableAlarmError",
|
|
30
|
-
{
|
|
31
|
-
operation: Schema.String,
|
|
32
|
-
message: Schema.String,
|
|
33
|
-
cause: Schema.optionalKey(Schema.Defect()),
|
|
34
|
-
},
|
|
35
|
-
) {}
|
|
36
|
-
|
|
37
|
-
const alarmFailure =
|
|
38
|
-
(operation: string) =>
|
|
39
|
-
(cause: unknown): DurableAlarmError =>
|
|
40
|
-
DurableAlarmError.make({
|
|
41
|
-
operation,
|
|
42
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
43
|
-
cause,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
/** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
|
|
47
|
-
export class DurableAlarmService extends Context.Service<
|
|
48
|
-
DurableAlarmService,
|
|
49
|
-
{
|
|
50
|
-
/** The scheduled deadline in epoch milliseconds, if any. */
|
|
51
|
-
readonly scheduled: Effect.Effect<Option.Option<number>, DurableAlarmError>;
|
|
52
|
-
/** Replace the slot with this deadline. */
|
|
53
|
-
readonly scheduleAt: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
54
|
-
/** Keep the EARLIER of the existing deadline and this one (the multiplexing rule). */
|
|
55
|
-
readonly ensureScheduledBy: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
|
|
56
|
-
/**
|
|
57
|
-
* Arm an immediate alarm (the durable, coalescing local wake) — DEFERRED while a
|
|
58
|
-
* maintenance pass is executing. Workerd cancels an in-flight alarm handler when a new
|
|
59
|
-
* EARLIER deadline is written during its execution (`requestScheduledAlarm`), and the
|
|
60
|
-
* maintenance pass runs INSIDE the alarm handler: an immediate wake landing mid-pass
|
|
61
|
-
* (a routed port mutation, a sibling's `wake()`, the coordinator's own local notify)
|
|
62
|
-
* would kill the running Attempt — manufacturing an ownership loss no real eviction
|
|
63
|
-
* caused, and routing open uncertain-class Tool Calls into spurious Unknown Outcomes.
|
|
64
|
-
* Deferral is contract-safe: wakes are droppable hints, every mutating entry point
|
|
65
|
-
* pre-arms BEFORE its first durable mutation (the alarm invariant never rests on this
|
|
66
|
-
* call), and the deferred wake is flushed when the pass completes.
|
|
67
|
-
*/
|
|
68
|
-
readonly scheduleNow: Effect.Effect<void, DurableAlarmError>;
|
|
69
|
-
/**
|
|
70
|
-
* Run one maintenance pass with wake deferral (see `scheduleNow`): `scheduleNow` calls
|
|
71
|
-
* while `body` executes coalesce into one flag, flushed as an immediate re-arm after the
|
|
72
|
-
* pass — failures of the flush are logged and swallowed (hints are droppable; the pass's
|
|
73
|
-
* own re-arm policy already bounded the next delivery).
|
|
74
|
-
*/
|
|
75
|
-
readonly withWakesDeferred: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
76
|
-
/** Clear the slot; only the maintenance pass does this, and only when all work settled. */
|
|
77
|
-
readonly cancel: Effect.Effect<void, DurableAlarmError>;
|
|
78
|
-
}
|
|
79
|
-
>()("@effect-agent/platform-cloudflare/DurableAlarmService") {
|
|
80
|
-
static readonly layer: Layer.Layer<DurableAlarmService, never, DurableObjectContext> =
|
|
81
|
-
Layer.effect(DurableAlarmService)(
|
|
82
|
-
Effect.gen(function* () {
|
|
83
|
-
const { ctx } = yield* DurableObjectContext;
|
|
84
|
-
/**
|
|
85
|
-
* In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
|
|
86
|
-
* running pass, and a deferred wake lost to eviction was only ever a promptness hint
|
|
87
|
-
* on top of the already-committed pre-armed alarm.
|
|
88
|
-
*/
|
|
89
|
-
const runningPasses = yield* Ref.make(0);
|
|
90
|
-
const wakeDeferred = yield* Ref.make(false);
|
|
91
|
-
const scheduled = Effect.tryPromise({
|
|
92
|
-
try: () => ctx.storage.getAlarm(),
|
|
93
|
-
catch: alarmFailure("get alarm"),
|
|
94
|
-
}).pipe(
|
|
95
|
-
Effect.map((deadline) =>
|
|
96
|
-
deadline === null ? Option.none<number>() : Option.some(deadline),
|
|
97
|
-
),
|
|
98
|
-
);
|
|
99
|
-
const scheduleAt = (epochMillis: number) =>
|
|
100
|
-
Effect.tryPromise({
|
|
101
|
-
try: () => ctx.storage.setAlarm(epochMillis),
|
|
102
|
-
catch: alarmFailure("set alarm"),
|
|
103
|
-
});
|
|
104
|
-
const ensureScheduledBy = (epochMillis: number) =>
|
|
105
|
-
scheduled.pipe(
|
|
106
|
-
Effect.flatMap((existing) =>
|
|
107
|
-
Option.isSome(existing) && existing.value <= epochMillis
|
|
108
|
-
? Effect.void
|
|
109
|
-
: scheduleAt(epochMillis),
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
const armNow = Clock.currentTimeMillis.pipe(
|
|
113
|
-
Effect.flatMap((now) => ensureScheduledBy(now)),
|
|
114
|
-
);
|
|
115
|
-
const scheduleNow = Ref.get(runningPasses).pipe(
|
|
116
|
-
Effect.flatMap((passes) => (passes > 0 ? Ref.set(wakeDeferred, true) : armNow)),
|
|
117
|
-
);
|
|
118
|
-
/**
|
|
119
|
-
* Flush after the LAST concurrent pass: the re-arm lands at the very end of the alarm
|
|
120
|
-
* handler, where a superseding cancellation only re-delivers to the already-idempotent
|
|
121
|
-
* pass. Runs inside `Effect.ensuring`, so flush failures are logged, never raised.
|
|
122
|
-
*/
|
|
123
|
-
const flushDeferredWake = Ref.get(runningPasses).pipe(
|
|
124
|
-
Effect.flatMap((passes) =>
|
|
125
|
-
passes > 0
|
|
126
|
-
? Effect.void
|
|
127
|
-
: Ref.getAndSet(wakeDeferred, false).pipe(
|
|
128
|
-
Effect.flatMap((wanted) => (wanted ? armNow : Effect.void)),
|
|
129
|
-
),
|
|
130
|
-
),
|
|
131
|
-
Effect.catch((error) =>
|
|
132
|
-
Effect.logWarning("DurableAlarmService: deferred wake flush failed", error),
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
const withWakesDeferred = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
136
|
-
Ref.update(runningPasses, (passes) => passes + 1).pipe(
|
|
137
|
-
Effect.andThen(body),
|
|
138
|
-
Effect.ensuring(
|
|
139
|
-
Ref.update(runningPasses, (passes) => passes - 1).pipe(
|
|
140
|
-
Effect.andThen(flushDeferredWake),
|
|
141
|
-
),
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
const cancel = Effect.tryPromise({
|
|
145
|
-
try: () => ctx.storage.deleteAlarm(),
|
|
146
|
-
catch: alarmFailure("delete alarm"),
|
|
147
|
-
});
|
|
148
|
-
return DurableAlarmService.of({
|
|
149
|
-
scheduled,
|
|
150
|
-
scheduleAt,
|
|
151
|
-
ensureScheduledBy,
|
|
152
|
-
scheduleNow,
|
|
153
|
-
withWakesDeferred,
|
|
154
|
-
cancel,
|
|
155
|
-
});
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
|
|
161
|
-
export class MaintenancePassReport extends Schema.Class<MaintenancePassReport>(
|
|
162
|
-
"@effect-agent/platform-cloudflare/MaintenancePassReport",
|
|
163
|
-
)({
|
|
164
|
-
/** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
|
|
165
|
-
recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
166
|
-
/** Settlements the drain pass finalized. */
|
|
167
|
-
settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
168
|
-
/** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
|
|
169
|
-
nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
170
|
-
/** `rearmed` while nonterminal work remains, `cleared` once everything settled. */
|
|
171
|
-
alarm: Schema.Literals(["rearmed", "cleared"]),
|
|
172
|
-
}) {}
|
|
173
|
-
|
|
174
|
-
export type MaintenancePassFailure =
|
|
175
|
-
| DurableWorkerFailure
|
|
176
|
-
| DurableBindingFailure
|
|
177
|
-
| DurableAlarmError;
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* The idempotent maintenance pass and the alarm-invariant helpers (plan §1.4, D-P6-1/2).
|
|
181
|
-
*
|
|
182
|
-
* `pass` = pre-arm → `runRecovery` → `processConversationResolved` → re-arm-or-clear:
|
|
183
|
-
*
|
|
184
|
-
* 1. **Pre-arm**: the alarm is re-armed at `now + wakeScanInterval` BEFORE any work, so an
|
|
185
|
-
* eviction (or thrown failure → workerd alarm retry) at any point of the pass leaves a
|
|
186
|
-
* committed alarm and the fresh incarnation converges without an incoming request.
|
|
187
|
-
* 2. **Reconcile before new work** (exit gate): every pass classifies and repairs every
|
|
188
|
-
* nonterminal Submission before the drain claims anything; the pure classifier and the
|
|
189
|
-
* repair executors are idempotent, so at-least-once alarm delivery re-runs them safely.
|
|
190
|
-
* 3. **Drain**: one bounded `processConversationResolved` pass over this Object's lane — the
|
|
191
|
-
* D-P6-1 shape; no infinite `runResolvedWorker` loop ever pins the Object.
|
|
192
|
-
* 4. **Re-arm policy**: nonterminal work re-arms at `now + min(backoff-with-jitter,
|
|
193
|
-
* wakeScanInterval)` — the scan interval bounds every wait (lease expiry of a dead
|
|
194
|
-
* incarnation included, since claims retry each pass) and the backoff (reset on progress,
|
|
195
|
-
* grown otherwise) keeps stuck lanes from busy-spinning; all settled clears the slot.
|
|
196
|
-
*
|
|
197
|
-
* Every step is a durability-protocol step that already tolerates re-execution, which is
|
|
198
|
-
* what makes double-fired alarms harmless (the alarm.test.ts gate).
|
|
199
|
-
*/
|
|
200
|
-
export class ConversationMaintenance extends Context.Service<
|
|
201
|
-
ConversationMaintenance,
|
|
202
|
-
{
|
|
203
|
-
/** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */
|
|
204
|
-
readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
|
|
205
|
-
/**
|
|
206
|
-
* Defensive half of the alarm invariant, run by the constructor gate: if any local
|
|
207
|
-
* Submission is nonterminal and no alarm is scheduled, arm one within the scan interval.
|
|
208
|
-
* Local-only (one ledger scan + the alarm slot) — never a recovery pass, never transport.
|
|
209
|
-
*/
|
|
210
|
-
readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
|
|
211
|
-
/**
|
|
212
|
-
* The pre-arm every mutating entry point runs BEFORE its first durable mutation: with
|
|
213
|
-
* the alarm committed first, a committed admission (or any other committed nonterminal
|
|
214
|
-
* transition) can never be observed without the alarm that will finish it.
|
|
215
|
-
*/
|
|
216
|
-
readonly preArm: Effect.Effect<void, DurableAlarmError>;
|
|
217
|
-
}
|
|
218
|
-
>()("@effect-agent/platform-cloudflare/ConversationMaintenance") {
|
|
219
|
-
static readonly layer: Layer.Layer<
|
|
220
|
-
ConversationMaintenance,
|
|
221
|
-
never,
|
|
222
|
-
| DurableAgentRuntime
|
|
223
|
-
| AgentBindingResolver
|
|
224
|
-
| SubmissionLedger
|
|
225
|
-
| DurableAlarmService
|
|
226
|
-
| CloudflareDurableRuntimeConfig
|
|
227
|
-
| ConversationObjectIdentity
|
|
228
|
-
> = Layer.effect(ConversationMaintenance)(
|
|
229
|
-
Effect.gen(function* () {
|
|
230
|
-
const runtime = yield* DurableAgentRuntime;
|
|
231
|
-
const resolver = yield* AgentBindingResolver;
|
|
232
|
-
const ledger = yield* SubmissionLedger;
|
|
233
|
-
const alarm = yield* DurableAlarmService;
|
|
234
|
-
const config = yield* CloudflareDurableRuntimeConfig;
|
|
235
|
-
const identity = yield* ConversationObjectIdentity;
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
|
|
239
|
-
* restarts at zero and merely re-arms sooner than a long-lived one would have.
|
|
240
|
-
*/
|
|
241
|
-
const stalls = yield* Ref.make(0);
|
|
242
|
-
|
|
243
|
-
const preArm = Clock.currentTimeMillis.pipe(
|
|
244
|
-
Effect.flatMap((now) => alarm.ensureScheduledBy(now + config.wakeScanInterval)),
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
const countNonterminal = Stream.runCollect(ledger.scanNonterminal).pipe(
|
|
248
|
-
Effect.map((snapshots) => snapshots.length),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const rearmDelay = Effect.fn("ConversationMaintenance.rearmDelay")(function* (
|
|
252
|
-
progressed: boolean,
|
|
253
|
-
) {
|
|
254
|
-
const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
|
|
255
|
-
progressed ? 0 : count + 1,
|
|
256
|
-
);
|
|
257
|
-
if (progressed) return config.alarmBackoffBase;
|
|
258
|
-
const exponent = Math.min(priorStalls, 30);
|
|
259
|
-
const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
|
|
260
|
-
const jitter = yield* Random.next;
|
|
261
|
-
// Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
|
|
262
|
-
// waiting longer than the deterministic bound.
|
|
263
|
-
const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
|
|
264
|
-
return Math.min(jittered, config.wakeScanInterval);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const pass = Effect.fn("ConversationMaintenance.pass")(function* (): Effect.fn.Return<
|
|
268
|
-
MaintenancePassReport,
|
|
269
|
-
MaintenancePassFailure
|
|
270
|
-
> {
|
|
271
|
-
// Step 1 — pre-arm: an abort or throw anywhere below leaves a committed alarm.
|
|
272
|
-
yield* preArm;
|
|
273
|
-
// Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
|
|
274
|
-
const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
|
|
275
|
-
// Step 3 — one bounded drain pass over this Object's own lane.
|
|
276
|
-
const settlements = yield* runtime
|
|
277
|
-
.processConversationResolved(identity.conversationId)
|
|
278
|
-
.pipe(Effect.provideService(AgentBindingResolver, resolver));
|
|
279
|
-
// Step 4 — re-arm or clear.
|
|
280
|
-
const nonterminal = yield* countNonterminal;
|
|
281
|
-
if (nonterminal === 0) {
|
|
282
|
-
yield* alarm.cancel;
|
|
283
|
-
yield* Ref.set(stalls, 0);
|
|
284
|
-
// Close the cancel/admission interleaving window: a submission committing between
|
|
285
|
-
// the count and the cancel (Durable Object events interleave at storage-operation
|
|
286
|
-
// boundaries) must not be left without its alarm. The recount re-arms if anything
|
|
287
|
-
// appeared; the submit entry's own `scheduleNow` covers commits after the recount.
|
|
288
|
-
const appeared = yield* countNonterminal;
|
|
289
|
-
if (appeared > 0) {
|
|
290
|
-
yield* preArm;
|
|
291
|
-
return MaintenancePassReport.make({
|
|
292
|
-
recovered: recovered.length,
|
|
293
|
-
settled: settlements.length,
|
|
294
|
-
nonterminal: appeared,
|
|
295
|
-
alarm: "rearmed",
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
return MaintenancePassReport.make({
|
|
299
|
-
recovered: recovered.length,
|
|
300
|
-
settled: settlements.length,
|
|
301
|
-
nonterminal,
|
|
302
|
-
alarm: "cleared",
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
const progressed =
|
|
306
|
-
settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
|
|
307
|
-
const delay = yield* rearmDelay(progressed);
|
|
308
|
-
const now = yield* Clock.currentTimeMillis;
|
|
309
|
-
yield* alarm.ensureScheduledBy(now + delay);
|
|
310
|
-
return MaintenancePassReport.make({
|
|
311
|
-
recovered: recovered.length,
|
|
312
|
-
settled: settlements.length,
|
|
313
|
-
nonterminal,
|
|
314
|
-
alarm: "rearmed",
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
|
|
319
|
-
const nonterminal = yield* countNonterminal;
|
|
320
|
-
if (nonterminal === 0) return;
|
|
321
|
-
yield* preArm;
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
return ConversationMaintenance.of({
|
|
325
|
-
// Wake deferral (see `DurableAlarmService.scheduleNow`): an immediate wake written
|
|
326
|
-
// while THIS handler runs would make workerd cancel it mid-Attempt; deferring keeps
|
|
327
|
-
// the running pass alive and flushes the hint as the pass's final re-arm.
|
|
328
|
-
pass: alarm.withWakesDeferred(pass()),
|
|
329
|
-
ensureAlarm: ensureAlarm(),
|
|
330
|
-
preArm,
|
|
331
|
-
});
|
|
332
|
-
}),
|
|
333
|
-
);
|
|
334
|
-
}
|
package/src/bindings.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import type { ConversationId } from "@effect-agent/core";
|
|
2
|
-
import type { ProducerId } from "@effect-agent/session";
|
|
3
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
|
|
7
|
-
* are supplied as Effect services/Layers"). Application code never reads `env` or touches a
|
|
8
|
-
* `DurableObjectState` directly — the Conversation Object class constructs these Layers once
|
|
9
|
-
* per incarnation and everything downstream consumes the services.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */
|
|
13
|
-
export class CloudflareBindingError extends Schema.TaggedError<CloudflareBindingError>()(
|
|
14
|
-
"CloudflareBindingError",
|
|
15
|
-
{
|
|
16
|
-
binding: Schema.String,
|
|
17
|
-
message: Schema.String,
|
|
18
|
-
},
|
|
19
|
-
) {}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The RPC surface one Conversation Durable Object exposes to Workers and to sibling
|
|
23
|
-
* Conversation Objects. `makeConversationObjectClass` implements it; the Worker-side client
|
|
24
|
-
* and the cross-Object transport call it through `DurableObjectNamespace` stubs. Every
|
|
25
|
-
* `encoded` value is a Schema-encoded envelope (`client.ts` wire schemas for host entry
|
|
26
|
-
* points, `@effect-agent/storage-cloudflare` port envelopes for `portCall`), so the RPC
|
|
27
|
-
* boundary carries only structured-cloneable JSON.
|
|
28
|
-
*/
|
|
29
|
-
export interface ConversationObjectRpc extends Rpc.DurableObjectBranded {
|
|
30
|
-
/** Admission-limits gate + `DurableAgentRuntime.submit`; answers a `SubmitResponse`. */
|
|
31
|
-
submitEncoded(encoded: unknown): Promise<unknown>;
|
|
32
|
-
/** Wake-hinted, poll-guaranteed settlement wait; answers an `AwaitSettlementResponse`. */
|
|
33
|
-
awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
|
|
34
|
-
/** One bounded page of canonical records; answers an `ObservePageResponse`. */
|
|
35
|
-
observePage(encoded: unknown): Promise<unknown>;
|
|
36
|
-
/** Durable abort intent; answers an `AbortResponse`. */
|
|
37
|
-
abortEncoded(encoded: unknown): Promise<unknown>;
|
|
38
|
-
/** Durable approval decision (plan §2.6); answers a `ResolveApprovalResponse`. */
|
|
39
|
-
resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
|
|
40
|
-
/** Authorized DUR-017 Unknown-Outcome resolution; answers a `ResolveUnknownResponse`. */
|
|
41
|
-
resolveUnknownEncoded(encoded: unknown): Promise<unknown>;
|
|
42
|
-
/** Owner-side cross-Object port endpoint (WP2 envelopes, executed on LOCAL facets). */
|
|
43
|
-
portCall(encoded: unknown): Promise<unknown>;
|
|
44
|
-
/** Droppable liveness hint from another Object: arms an immediate alarm. */
|
|
45
|
-
wake(): Promise<void>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The `DurableObjectNamespace` binding that addresses Conversation Objects. The Object
|
|
50
|
-
* identity rule is `namespace.idFromName(conversationId)` (plan §1.2): Conversation IDs are
|
|
51
|
-
* globally unique, so the mapping is total and deterministic and no directory service exists.
|
|
52
|
-
*/
|
|
53
|
-
export class ConversationObjectNamespace extends Context.Service<
|
|
54
|
-
ConversationObjectNamespace,
|
|
55
|
-
{
|
|
56
|
-
readonly namespace: DurableObjectNamespace<ConversationObjectRpc>;
|
|
57
|
-
}
|
|
58
|
-
>()("@effect-agent/platform-cloudflare/ConversationObjectNamespace") {
|
|
59
|
-
static layer(
|
|
60
|
-
namespace: DurableObjectNamespace<ConversationObjectRpc>,
|
|
61
|
-
): Layer.Layer<ConversationObjectNamespace> {
|
|
62
|
-
return Layer.succeed(ConversationObjectNamespace)({ namespace });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,
|
|
68
|
-
* and a namespace binding is a host object no Schema can decode, so this is the documented
|
|
69
|
-
* narrowest-boundary check (structural probe for the namespace surface the transport uses);
|
|
70
|
-
* a missing or misshaped binding fails typed before any Layer is built.
|
|
71
|
-
*/
|
|
72
|
-
export const conversationNamespaceFromEnv = (
|
|
73
|
-
env: unknown,
|
|
74
|
-
binding: string,
|
|
75
|
-
): Effect.Effect<DurableObjectNamespace<ConversationObjectRpc>, CloudflareBindingError> =>
|
|
76
|
-
Effect.suspend(() => {
|
|
77
|
-
if (typeof env !== "object" || env === null) {
|
|
78
|
-
return Effect.fail(
|
|
79
|
-
CloudflareBindingError.make({
|
|
80
|
-
binding,
|
|
81
|
-
message: "The Worker environment is not an object; no bindings are available.",
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
const candidate: unknown = (env as Record<string, unknown>)[binding];
|
|
86
|
-
if (
|
|
87
|
-
typeof candidate === "object" &&
|
|
88
|
-
candidate !== null &&
|
|
89
|
-
"idFromName" in candidate &&
|
|
90
|
-
typeof candidate.idFromName === "function" &&
|
|
91
|
-
"get" in candidate &&
|
|
92
|
-
typeof candidate.get === "function"
|
|
93
|
-
) {
|
|
94
|
-
// The structural probe above is the entire runtime contract this package relies on;
|
|
95
|
-
// the assertion records that `idFromName`/`get` name a DurableObjectNamespace.
|
|
96
|
-
return Effect.succeed(candidate as DurableObjectNamespace<ConversationObjectRpc>);
|
|
97
|
-
}
|
|
98
|
-
return Effect.fail(
|
|
99
|
-
CloudflareBindingError.make({
|
|
100
|
-
binding,
|
|
101
|
-
message:
|
|
102
|
-
`env.${binding} is not a DurableObjectNamespace binding; declare the Conversation ` +
|
|
103
|
-
"Object class under this binding in the Worker configuration.",
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
/** `ConversationObjectNamespace` built from the untyped Worker `env` (fails typed). */
|
|
109
|
-
export const conversationNamespaceLayer = (
|
|
110
|
-
env: unknown,
|
|
111
|
-
binding: string,
|
|
112
|
-
): Layer.Layer<ConversationObjectNamespace, CloudflareBindingError> =>
|
|
113
|
-
Layer.effect(ConversationObjectNamespace)(
|
|
114
|
-
Effect.map(conversationNamespaceFromEnv(env, binding), (namespace) => ({ namespace })),
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The live Durable Object execution context of THIS incarnation. Only Layer construction and
|
|
119
|
-
* the alarm service consume it; important state never lives on it (`ctx.storage` is truth,
|
|
120
|
-
* everything in memory is a cache — deployment spec §11).
|
|
121
|
-
*/
|
|
122
|
-
export class DurableObjectContext extends Context.Service<
|
|
123
|
-
DurableObjectContext,
|
|
124
|
-
{
|
|
125
|
-
readonly ctx: DurableObjectState;
|
|
126
|
-
readonly env: unknown;
|
|
127
|
-
}
|
|
128
|
-
>()("@effect-agent/platform-cloudflare/DurableObjectContext") {
|
|
129
|
-
static layer(ctx: DurableObjectState, env: unknown): Layer.Layer<DurableObjectContext> {
|
|
130
|
-
return Layer.succeed(DurableObjectContext)({ ctx, env });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* The Conversation identity this Object serializes and the producer identity its Attempts
|
|
136
|
-
* write with (`{producerPrefix}:{conversationId}`, plan §1.4). Derived once per incarnation
|
|
137
|
-
* from `ctx.id.name` — the Object identity rule guarantees the name IS the Conversation ID.
|
|
138
|
-
*/
|
|
139
|
-
export class ConversationObjectIdentity extends Context.Service<
|
|
140
|
-
ConversationObjectIdentity,
|
|
141
|
-
{
|
|
142
|
-
readonly conversationId: ConversationId;
|
|
143
|
-
readonly producerId: ProducerId;
|
|
144
|
-
}
|
|
145
|
-
>()("@effect-agent/platform-cloudflare/ConversationObjectIdentity") {}
|