effect-durable-agent-cloudflare 0.1.0-alpha.10

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.
@@ -0,0 +1,1459 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { DurableEventEnvelope, assistantMessageCommittedEventType, assistantMessageImportedEventType, assistantPartialCommittedEventType, commandAdmittedEventType, commandCancelledEventType, commandCompletedEventType, commandFailedEventType, commandStartedEventType, contextRebasedEventType, decodeUnknownEDADurableEventSync, effectDurableAgentNamespace, steeringMessageQueuedEventType, summaryCreatedEventType, systemMessageCommittedEventType, userMessageCommittedEventType } from "effect-durable-agent/types/events";
4
+ import { SequenceNumber, SessionId, SummaryId, durablePosition } from "effect-durable-agent/types/core";
5
+ import { EDATraceMetadata, makeRootEDATraceMetadata } from "effect-durable-agent/types/tracing";
6
+ import { annotateEdaSpan, committedBatchAttributes, compactSpanAttributes, toEdaExternalSpan } from "effect-durable-agent/services/tracing";
7
+ import * as Layer from "effect/Layer";
8
+ import * as ManagedRuntime from "effect/ManagedRuntime";
9
+ import { EDARuntime } from "effect-durable-agent/services/runtime";
10
+ import { CommittedDurableEvent, EDASessionStore, EDASessionStoreError } from "effect-durable-agent/services/session-store";
11
+ import { SinkCheckpointStore } from "effect-durable-agent/services/sink-checkpoint-store";
12
+ import * as Cause from "effect/Cause";
13
+ import * as Option from "effect/Option";
14
+ import * as Stream from "effect/Stream";
15
+ import { CompactionSummaryArtifact } from "effect-durable-agent/domain/context-projection";
16
+ import { RunSchedulingWakeup } from "effect-durable-agent/services/run-scheduling-wakeup";
17
+ import "effect-durable-agent/services/compaction";
18
+ import { EDAKeepAlive } from "effect-durable-agent/services/keep-alive";
19
+ import "effect-durable-agent/services/prompt-projector";
20
+ import { EDAReducerRegistry } from "effect-durable-agent/services/reducer-registry";
21
+ import { makeEDARuntimeLayer } from "effect-durable-agent/services/runtime-layer";
22
+ import { SessionEventObserver } from "effect-durable-agent/services/session-event-observer";
23
+ import { EDAWebSocketDeliveryCheckpoint, EDA_WEB_SOCKET_PING_MESSAGE, EDA_WEB_SOCKET_PONG_MESSAGE, EDA_WS_CLOSE_PROTOCOL_ERROR, EDA_WS_CLOSE_SEND_FAILED, SubscriberId, checkpointWebSocketDeliveryState, decodeEDAWebSocketClientMessage, defaultEDAWebSocketFlowControl, deliveryHelloFrame, encodeEDAWebSocketServerFrame, makeWebSocketDeliveryState, onClientAck, onDurableEvents, onEphemeralEvent, onHostSuppressedFrame, restoreWebSocketDeliveryState } from "effect-durable-agent/websocket";
24
+ /**
25
+ * Alarm-backed in-memory keepalive for a raw Durable Object host.
26
+ *
27
+ * The active lease count is intentionally not durable: if the object is evicted
28
+ * or crashes, the fibers the leases represented are gone too, and normal EDA
29
+ * durable recovery must explain any escaped lifecycle boundaries.
30
+ */
31
+ var DurableObjectKeepAlive = class {
32
+ storage;
33
+ background;
34
+ intervalMs;
35
+ now;
36
+ onBackgroundError;
37
+ activeLeases = 0;
38
+ runSchedulingPending = false;
39
+ generation = 0;
40
+ scheduleTail = Promise.resolve();
41
+ shutdownRequested = false;
42
+ constructor(storage, background = void 0, options = {}) {
43
+ this.storage = storage;
44
+ this.background = background;
45
+ this.intervalMs = options.intervalMs ?? 3e4;
46
+ this.now = options.now ?? (() => Date.now());
47
+ this.onBackgroundError = options.onBackgroundError ?? (() => void 0);
48
+ }
49
+ /** Number of active unsettled host roots currently holding the heartbeat. */
50
+ get activeLeaseCount() {
51
+ return this.activeLeases;
52
+ }
53
+ /** Durable delivery work survives the release of every in-memory active lease. */
54
+ setRunSchedulingPending(pending) {
55
+ this.runSchedulingPending = pending;
56
+ return this.reschedule();
57
+ }
58
+ /** Acquire one active-work lease and arm the heartbeat if this is the first lease. */
59
+ acquire() {
60
+ if (this.shutdownRequested) return Promise.reject(/* @__PURE__ */ new Error("DurableObjectKeepAlive is shut down"));
61
+ this.activeLeases += 1;
62
+ const generation = this.generation;
63
+ let released = false;
64
+ return this.reschedule().then(() => ({ release: async () => {
65
+ if (released) return;
66
+ released = true;
67
+ if (generation !== this.generation) return;
68
+ this.activeLeases = Math.max(0, this.activeLeases - 1);
69
+ await this.reschedule();
70
+ } }), async (error) => {
71
+ released = true;
72
+ if (generation !== this.generation) throw error;
73
+ this.activeLeases = Math.max(0, this.activeLeases - 1);
74
+ await this.reschedule().catch(this.onBackgroundError);
75
+ throw error;
76
+ });
77
+ }
78
+ /** Run a promise while holding a lease. Intended for host adapters, not EDA internals. */
79
+ async runWhileActive(fn) {
80
+ const lease = await this.acquire();
81
+ try {
82
+ return await fn();
83
+ } finally {
84
+ await lease.release();
85
+ }
86
+ }
87
+ /**
88
+ * Track an already-forked Effect fiber as active host work until it settles.
89
+ *
90
+ * Use this with `ManagedRuntime.runFork(...)` when the fiber needs an already
91
+ * built runtime context. The returned lease can be released manually, but it
92
+ * is normally released by the fiber observer.
93
+ */
94
+ async trackFiber(fiber) {
95
+ const lease = await this.acquire();
96
+ fiber.addObserver(() => {
97
+ this.waitUntil(lease.release().catch((error) => {
98
+ this.onBackgroundError(error);
99
+ }));
100
+ });
101
+ if (fiber.pollUnsafe() !== void 0) await lease.release();
102
+ return lease;
103
+ }
104
+ /**
105
+ * Fork an Effect as host-tracked active work.
106
+ *
107
+ * This mirrors Effect's host-runner pattern: observe the root fiber and release
108
+ * the alarm lease when it settles. It deliberately does not patch Effect
109
+ * internals or try to classify runnable vs suspended fibers.
110
+ */
111
+ async fork(effect) {
112
+ const fiber = Effect.runFork(effect);
113
+ await this.trackFiber(fiber);
114
+ return fiber;
115
+ }
116
+ /** Handle the Durable Object `alarm()` callback and re-arm while work remains. */
117
+ alarm() {
118
+ return this.reschedule();
119
+ }
120
+ /** Permanently stop the heartbeat and prevent later lease releases from re-arming it. */
121
+ shutdown() {
122
+ this.shutdownRequested = true;
123
+ this.generation += 1;
124
+ this.activeLeases = 0;
125
+ this.runSchedulingPending = false;
126
+ return this.reschedule();
127
+ }
128
+ /** Start a fresh lease generation after the host intentionally resets its durable state. */
129
+ restart() {
130
+ this.shutdownRequested = false;
131
+ }
132
+ waitUntil(promise) {
133
+ if (this.background !== void 0) {
134
+ this.background.waitUntil(promise);
135
+ return;
136
+ }
137
+ }
138
+ reschedule() {
139
+ this.scheduleTail = this.scheduleTail.catch(() => void 0).then(() => this.syncAlarm());
140
+ return this.scheduleTail;
141
+ }
142
+ async syncAlarm() {
143
+ if (this.shutdownRequested || this.activeLeases === 0 && !this.runSchedulingPending) {
144
+ await this.storage.deleteAlarm();
145
+ return;
146
+ }
147
+ const desiredAt = this.now() + this.intervalMs;
148
+ const currentAt = await this.storage.getAlarm();
149
+ if (currentAt === null || currentAt > desiredAt) await this.storage.setAlarm(desiredAt);
150
+ }
151
+ };
152
+ //#endregion
153
+ //#region src/durable-object-sink-checkpoints.ts
154
+ /**
155
+ * Durable Object SQLite adapter for EDA sink checkpoints.
156
+ *
157
+ * Checkpoint SQL stays in the host layer for the same reason session storage SQL
158
+ * does: framework sink code should depend on checkpoint semantics, not table access.
159
+ */
160
+ var DurableObjectSinkCheckpointStore = class DurableObjectSinkCheckpointStore {
161
+ /** Ensure the fixed `_eda_sink_cursors` table exists during host startup. */
162
+ static migrate = (storage) => migrateSinkCheckpointStore(storage);
163
+ /** Build the semantic checkpoint store over native DO SQLite. */
164
+ static make = (storage) => makeDurableObjectSinkCheckpointStore(storage);
165
+ static layer = (storage) => Layer.effect(SinkCheckpointStore, DurableObjectSinkCheckpointStore.make(storage));
166
+ };
167
+ /** Create the sink checkpoint table inside the current EDA storage schema. */
168
+ const migrateSinkCheckpointStore = (storage) => Effect.try({
169
+ try: () => {
170
+ storage.sql.exec(`
171
+ CREATE TABLE IF NOT EXISTS _eda_sink_cursors (
172
+ sink_name TEXT PRIMARY KEY,
173
+ after_seq INTEGER NOT NULL,
174
+ payload_json TEXT NOT NULL
175
+ )
176
+ `);
177
+ },
178
+ catch: (error) => sinkCheckpointError("migrating SinkCheckpointStore", error)
179
+ });
180
+ /** Interpret checkpoint load/save/commit with idempotent upserts. */
181
+ const makeDurableObjectSinkCheckpointStore = (storage) => Effect.gen(function* () {
182
+ yield* DurableObjectSinkCheckpointStore.migrate(storage);
183
+ const { sql } = storage;
184
+ return {
185
+ load: (sinkName) => Effect.try({
186
+ try: () => {
187
+ const row = sql.exec(`SELECT after_seq, payload_json FROM _eda_sink_cursors WHERE sink_name = ?`, sinkName).toArray()[0];
188
+ return row === void 0 ? {
189
+ afterSeq: SequenceNumber.make(0),
190
+ payload: void 0
191
+ } : {
192
+ afterSeq: SequenceNumber.make(row.after_seq),
193
+ payload: JSON.parse(row.payload_json)
194
+ };
195
+ },
196
+ catch: (error) => sinkCheckpointError("reading sink checkpoint", error)
197
+ }),
198
+ saveState: (sinkName, payload) => Effect.try({
199
+ try: () => {
200
+ sql.exec(`
201
+ INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
202
+ VALUES (?, 0, ?)
203
+ ON CONFLICT(sink_name) DO UPDATE SET
204
+ payload_json = excluded.payload_json
205
+ `, sinkName, JSON.stringify(payload));
206
+ },
207
+ catch: (error) => sinkCheckpointError("saving sink checkpoint state", error)
208
+ }),
209
+ commit: (sinkName, afterSeq, payload) => Effect.try({
210
+ try: () => {
211
+ sql.exec(`
212
+ INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
213
+ VALUES (?, ?, ?)
214
+ ON CONFLICT(sink_name) DO UPDATE SET
215
+ after_seq = excluded.after_seq,
216
+ payload_json = excluded.payload_json
217
+ `, sinkName, afterSeq, JSON.stringify(payload ?? { updatedAtMs: Date.now() }));
218
+ },
219
+ catch: (error) => sinkCheckpointError("committing sink checkpoint", error)
220
+ })
221
+ };
222
+ });
223
+ const sinkCheckpointError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
224
+ //#endregion
225
+ //#region src/durable-object-store.ts
226
+ /** Safe payload ceiling below the Durable Object SQLite 2 MB value limit. */
227
+ /** Conservative JSON limit below the Durable Object SQLite per-value ceiling. */
228
+ const durableObjectSerializedJsonHardCapBytes = 15e5;
229
+ const ProjectionPayload = Schema.Record(Schema.String, Schema.Unknown);
230
+ const AdmittedCommandProjection = Schema.Struct({
231
+ commandId: Schema.Unknown,
232
+ idempotencyKey: Schema.optionalKey(Schema.Unknown)
233
+ });
234
+ /** Single squashed schema version for the current pre-release DO store. */
235
+ const durableObjectSessionStoreSchemaVersion = 1;
236
+ const jsonEncoder = new TextEncoder();
237
+ /**
238
+ * Bound one synchronous DO SQLite event-log read/decode to a small CPU and memory slice.
239
+ *
240
+ * A single logical event can already approach the per-row JSON hard cap, and logical decode may
241
+ * touch sidecar rows. Sixteen rows keeps worst-case page memory materially below isolate limits
242
+ * while still amortizing SQL cursor setup for normal small framework events. Revisit with page
243
+ * duration/byte telemetry before increasing.
244
+ */
245
+ const durableObjectEventReadPageRows = 16;
246
+ /**
247
+ * Durable Object SQLite-backed implementation of the semantic `EDASessionStore`.
248
+ *
249
+ * This adapter is the only place EDA framework storage code touches DO SQL. It
250
+ * translates domain append intent into synchronous SQLite writes while keeping
251
+ * `SessionState` and runners SQL-agnostic.
252
+ */
253
+ var DurableObjectSessionStore = class DurableObjectSessionStore {
254
+ /** Run the single squashed framework migration for the current pre-release schema. */
255
+ static migrate = (storage) => migrateDurableObjectSessionStore(storage);
256
+ /** Build a session-scoped store after migrations have completed. */
257
+ static make = ({ sessionId, storage }) => Effect.gen(function* () {
258
+ yield* DurableObjectSessionStore.migrate(storage);
259
+ return makeDurableObjectSessionStore(sessionId, storage);
260
+ });
261
+ /** Layer constructor used by the Durable Object host runtime graph. */
262
+ static layer = (options) => Layer.effect(EDASessionStore, DurableObjectSessionStore.make(options));
263
+ };
264
+ /**
265
+ * Create all framework-owned tables in one idempotent startup step.
266
+ *
267
+ * The project is still pre-release, so this remains a squashed version-1 schema
268
+ * rather than a chain of compatibility migrations.
269
+ */
270
+ const migrateDurableObjectSessionStore = (storage) => Effect.try({
271
+ try: () => storage.transactionSync(() => {
272
+ const { sql } = storage;
273
+ sql.exec(`
274
+ CREATE TABLE IF NOT EXISTS _eda_schema_migrations (
275
+ id INTEGER PRIMARY KEY,
276
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
277
+ )
278
+ `);
279
+ if ((sql.exec("SELECT COALESCE(MAX(id), 0) AS version FROM _eda_schema_migrations").one().version ?? 0) < durableObjectSessionStoreSchemaVersion) {
280
+ sql.exec(`
281
+ CREATE TABLE IF NOT EXISTS _eda_event_log (
282
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
283
+ event_id TEXT NOT NULL UNIQUE,
284
+ namespace TEXT NOT NULL,
285
+ type TEXT NOT NULL,
286
+ schema_version INTEGER NOT NULL,
287
+ created_at_ms INTEGER NOT NULL,
288
+ trace_json TEXT NOT NULL,
289
+ fact_json TEXT NOT NULL
290
+ )
291
+ `);
292
+ sql.exec(`
293
+ CREATE INDEX IF NOT EXISTS _eda_event_log_namespace_type_seq_idx
294
+ ON _eda_event_log(namespace, type, seq)
295
+ `);
296
+ sql.exec(`
297
+ CREATE TABLE IF NOT EXISTS _eda_command_state (
298
+ command_id TEXT PRIMARY KEY,
299
+ admitted_seq INTEGER NOT NULL,
300
+ status TEXT NOT NULL,
301
+ idempotency_key TEXT,
302
+ payload_json TEXT NOT NULL
303
+ )
304
+ `);
305
+ sql.exec(`
306
+ CREATE TABLE IF NOT EXISTS _eda_command_inputs (
307
+ command_id TEXT PRIMARY KEY,
308
+ admitted_seq INTEGER NOT NULL UNIQUE,
309
+ payload_json TEXT NOT NULL
310
+ )
311
+ `);
312
+ sql.exec(`
313
+ CREATE INDEX IF NOT EXISTS _eda_command_state_status_seq_idx
314
+ ON _eda_command_state(status, admitted_seq)
315
+ `);
316
+ sql.exec(`
317
+ CREATE UNIQUE INDEX IF NOT EXISTS _eda_command_state_idempotency_idx
318
+ ON _eda_command_state(idempotency_key) WHERE idempotency_key IS NOT NULL
319
+ `);
320
+ sql.exec(`
321
+ CREATE TABLE IF NOT EXISTS _eda_context_messages (
322
+ message_id TEXT PRIMARY KEY,
323
+ context_seq INTEGER NOT NULL UNIQUE,
324
+ payload_json TEXT NOT NULL
325
+ )
326
+ `);
327
+ sql.exec(`
328
+ CREATE INDEX IF NOT EXISTS _eda_context_messages_context_seq_idx
329
+ ON _eda_context_messages(context_seq)
330
+ `);
331
+ sql.exec(`
332
+ CREATE TABLE IF NOT EXISTS _eda_context_summaries (
333
+ summary_id TEXT PRIMARY KEY,
334
+ created_seq INTEGER NOT NULL UNIQUE,
335
+ payload_json TEXT NOT NULL
336
+ )
337
+ `);
338
+ sql.exec(`
339
+ CREATE INDEX IF NOT EXISTS _eda_context_summaries_created_seq_idx
340
+ ON _eda_context_summaries(created_seq)
341
+ `);
342
+ sql.exec(`
343
+ CREATE TABLE IF NOT EXISTS _eda_reducer_checkpoints (
344
+ reducer_name TEXT PRIMARY KEY,
345
+ schema_version INTEGER NOT NULL,
346
+ through_seq INTEGER NOT NULL,
347
+ payload_json TEXT NOT NULL,
348
+ updated_at_ms INTEGER NOT NULL
349
+ )
350
+ `);
351
+ sql.exec(`
352
+ CREATE TABLE IF NOT EXISTS _eda_sink_cursors (
353
+ sink_name TEXT PRIMARY KEY,
354
+ after_seq INTEGER NOT NULL,
355
+ payload_json TEXT NOT NULL
356
+ )
357
+ `);
358
+ sql.exec("INSERT INTO _eda_schema_migrations (id) VALUES (?)", durableObjectSessionStoreSchemaVersion);
359
+ }
360
+ }),
361
+ catch: (error) => durableStoreError("migrating DurableObjectSessionStore", error)
362
+ });
363
+ /**
364
+ * Interpret the semantic store API with native DO SQLite.
365
+ *
366
+ * All writes that affect durable ordering happen inside `transactionSync`; replay
367
+ * and lookup reads stay synchronous SQL wrapped in Effect boundaries.
368
+ */
369
+ const makeDurableObjectSessionStore = (sessionId, storage) => {
370
+ const { sql } = storage;
371
+ const ensureSession = (event) => {
372
+ if (event.sessionId !== sessionId) throw new Error(`DurableObjectSessionStore is scoped to session ${sessionId}; received ${event.sessionId}`);
373
+ };
374
+ /**
375
+ * Insert one append entry or return its existing idempotent event position.
376
+ *
377
+ * The event row is the sequence allocator. Artifact/projection writes happen
378
+ * only for newly inserted events, preventing duplicate appends from advancing
379
+ * body rows or metadata.
380
+ */
381
+ const insertOrRead = (entry) => {
382
+ const { event } = entry;
383
+ ensureSession(event);
384
+ const existing = readCommittedByEventIdOptional(sql, sessionId, event.eventId);
385
+ if (existing !== void 0) return existing;
386
+ const encoded = encodeEventLogRow(event);
387
+ sql.exec(`
388
+ INSERT INTO _eda_event_log (
389
+ event_id,
390
+ namespace,
391
+ type,
392
+ schema_version,
393
+ created_at_ms,
394
+ trace_json,
395
+ fact_json
396
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
397
+ `, encoded.eventId, encoded.namespace, encoded.type, encoded.schemaVersion, encoded.createdAtMs, encoded.traceJson, encoded.factJson);
398
+ const row = readEventLogRowByEventId(sql, event.eventId);
399
+ applySynchronousProjections(sql, entry, SequenceNumber.make(row.seq), encoded.factJson);
400
+ return rowToCommittedDurableEvent(sql, row, sessionId);
401
+ };
402
+ /** Execute an entire append batch as one synchronous SQLite transaction. */
403
+ const append = (batch) => Effect.try({
404
+ try: () => storage.transactionSync(() => batch.entries.map(insertOrRead)),
405
+ catch: (error) => durableStoreError("appending DurableObjectSessionStore batch", error)
406
+ }).pipe(Effect.tap((committed) => annotateEdaSpan(committedBatchAttributes(committed))));
407
+ return {
408
+ append,
409
+ eventsAfter: (afterSeq) => makeCommittedEventStream(sql, sessionId, afterSeq),
410
+ loadCommittedEventsBySeq: (seqs) => Effect.try({
411
+ try: () => uniqueSequenceNumbers(seqs).sort((left, right) => left - right).map((seq) => readCommittedBySeq(sql, sessionId, seq)),
412
+ catch: (error) => durableStoreError("loading DurableObjectSessionStore events by seq", error)
413
+ }),
414
+ findCommandAdmission: (input) => Effect.try({
415
+ try: () => {
416
+ const row = findCommandAdmissionRow(sql, input.commandId, input.idempotencyKey);
417
+ return row === void 0 ? void 0 : readCommittedBySeq(sql, sessionId, row.admitted_seq);
418
+ },
419
+ catch: (error) => durableStoreError("finding command admission", error)
420
+ }),
421
+ loadSummaryArtifact: (summaryId) => Effect.try({
422
+ try: () => readSummaryPayloadOptional(sql, summaryId),
423
+ catch: (error) => durableStoreError("loading DurableObjectSessionStore summary", error)
424
+ }),
425
+ loadReducerCheckpoint: (name) => Effect.try({
426
+ try: () => readReducerCheckpoint(sql, name),
427
+ catch: (error) => durableStoreError(`loading DurableObjectSessionStore reducer checkpoint ${name}`, error)
428
+ }),
429
+ saveReducerCheckpoint: (checkpoint) => Effect.try({
430
+ try: () => writeReducerCheckpoint(sql, checkpoint),
431
+ catch: (error) => durableStoreError(`saving DurableObjectSessionStore reducer checkpoint ${checkpoint.name}`, error)
432
+ }),
433
+ saveReducerCheckpoints: (checkpoints) => Effect.try({
434
+ try: () => storage.transactionSync(() => checkpoints.forEach((checkpoint) => writeReducerCheckpoint(sql, checkpoint))),
435
+ catch: (error) => durableStoreError("saving DurableObjectSessionStore reducer checkpoints", error)
436
+ })
437
+ };
438
+ };
439
+ const makeCommittedEventStream = (sql, sessionId, afterSeq) => Stream.fromPull(Effect.sync(() => {
440
+ let cursor = {
441
+ afterSeq,
442
+ throughSeq: void 0
443
+ };
444
+ let done = false;
445
+ return Effect.gen(function* () {
446
+ if (done) return yield* Cause.done();
447
+ yield* Effect.yieldNow;
448
+ const page = yield* readCommittedEventPage(sql, sessionId, cursor);
449
+ if (page.events.length === 0) {
450
+ done = true;
451
+ return yield* Cause.done();
452
+ }
453
+ if (Option.isSome(page.next)) cursor = page.next.value;
454
+ else done = true;
455
+ return page.events;
456
+ });
457
+ }));
458
+ const readCommittedEventPage = (sql, sessionId, cursor) => Effect.try({
459
+ try: () => {
460
+ const throughSeq = cursor.throughSeq ?? readEventLogHead(sql);
461
+ if (cursor.afterSeq >= throughSeq) return {
462
+ events: [],
463
+ next: Option.none()
464
+ };
465
+ const events = sql.exec(`
466
+ SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
467
+ FROM _eda_event_log
468
+ WHERE seq > ? AND seq <= ?
469
+ ORDER BY seq ASC
470
+ LIMIT ?
471
+ `, cursor.afterSeq, throughSeq, durableObjectEventReadPageRows).toArray().map((row) => rowToCommittedDurableEvent(sql, row, sessionId));
472
+ const lastSeq = events.at(-1)?.position.seq ?? cursor.afterSeq;
473
+ return {
474
+ events,
475
+ next: lastSeq < throughSeq ? Option.some({
476
+ afterSeq: lastSeq,
477
+ throughSeq
478
+ }) : Option.none()
479
+ };
480
+ },
481
+ catch: (error) => durableStoreError("replaying DurableObjectSessionStore events", error)
482
+ });
483
+ const readEventLogHead = (sql) => SequenceNumber.make(sql.exec(`
484
+ SELECT COALESCE(MAX(seq), 0) AS head
485
+ FROM _eda_event_log
486
+ `).one().head ?? 0);
487
+ const uniqueSequenceNumbers = (seqs) => Array.from(new Set(seqs.map((seq) => Number(seq)))).map((seq) => SequenceNumber.make(seq));
488
+ const readEventLogRowByEventId = (sql, eventId) => sql.exec(`
489
+ SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
490
+ FROM _eda_event_log
491
+ WHERE event_id = ?
492
+ `, eventId).one();
493
+ const readCommittedByEventIdOptional = (sql, sessionId, eventId) => {
494
+ const row = sql.exec(`
495
+ SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
496
+ FROM _eda_event_log
497
+ WHERE event_id = ?
498
+ `, eventId).toArray()[0];
499
+ return row === void 0 ? void 0 : rowToCommittedDurableEvent(sql, row, sessionId);
500
+ };
501
+ const readReducerCheckpoint = (sql, name) => {
502
+ const row = sql.exec(`
503
+ SELECT reducer_name, schema_version, through_seq, payload_json, updated_at_ms
504
+ FROM _eda_reducer_checkpoints
505
+ WHERE reducer_name = ?
506
+ `, name).toArray()[0];
507
+ return row === void 0 ? void 0 : {
508
+ name: row.reducer_name,
509
+ schemaVersion: row.schema_version,
510
+ throughSeq: SequenceNumber.make(row.through_seq),
511
+ payload: JSON.parse(row.payload_json),
512
+ updatedAtMs: row.updated_at_ms
513
+ };
514
+ };
515
+ const writeReducerCheckpoint = (sql, checkpoint) => {
516
+ sql.exec(`
517
+ INSERT INTO _eda_reducer_checkpoints (
518
+ reducer_name,
519
+ schema_version,
520
+ through_seq,
521
+ payload_json,
522
+ updated_at_ms
523
+ ) VALUES (?, ?, ?, ?, ?)
524
+ ON CONFLICT(reducer_name) DO UPDATE SET
525
+ schema_version = excluded.schema_version,
526
+ through_seq = excluded.through_seq,
527
+ payload_json = excluded.payload_json,
528
+ updated_at_ms = excluded.updated_at_ms
529
+ `, checkpoint.name, checkpoint.schemaVersion, checkpoint.throughSeq, stringifyJsonForColumn(checkpoint.payload, "_eda_reducer_checkpoints.payload_json"), checkpoint.updatedAtMs);
530
+ };
531
+ const readMessagePayloadByContextSeq = (sql, contextSeq) => {
532
+ const message = sql.exec(`
533
+ SELECT context_seq, payload_json
534
+ FROM _eda_context_messages
535
+ WHERE context_seq = ?
536
+ `, contextSeq).toArray()[0];
537
+ if (message === void 0) throw new Error(`Context message at seq ${contextSeq} missing from _eda_context_messages`);
538
+ return message;
539
+ };
540
+ const readCommittedBySeq = (sql, sessionId, seq) => {
541
+ return rowToCommittedDurableEvent(sql, sql.exec(`
542
+ SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
543
+ FROM _eda_event_log
544
+ WHERE seq = ?
545
+ `, seq).one(), sessionId);
546
+ };
547
+ const rowToCommittedDurableEvent = (sql, row, sessionId) => CommittedDurableEvent.make({
548
+ position: durablePosition(SequenceNumber.make(row.seq)),
549
+ event: decodeDurableEvent({
550
+ namespace: row.namespace,
551
+ type: row.type,
552
+ schemaVersion: row.schema_version,
553
+ durability: "durable",
554
+ eventId: row.event_id,
555
+ sessionId,
556
+ createdAtMs: row.created_at_ms,
557
+ trace: JSON.parse(row.trace_json),
558
+ payload: readLogicalPayload(sql, row)
559
+ })
560
+ });
561
+ const decodeDurableEvent = (input) => {
562
+ const event = Schema.decodeUnknownSync(DurableEventEnvelope)(input);
563
+ return event.namespace === effectDurableAgentNamespace ? decodeUnknownEDADurableEventSync(event) : event;
564
+ };
565
+ const encodeEventLogRow = (event) => {
566
+ const encoded = Schema.encodeSync(DurableEventEnvelope)(event);
567
+ return {
568
+ eventId: encoded.eventId,
569
+ namespace: encoded.namespace,
570
+ type: encoded.type,
571
+ schemaVersion: encoded.schemaVersion,
572
+ createdAtMs: encoded.createdAtMs,
573
+ traceJson: stringifyJsonForColumn(encoded.trace, "_eda_event_log.trace_json"),
574
+ factJson: stringifyJsonForColumn(eventLogFactPayload(encoded.namespace, encoded.type, encoded.payload), "_eda_event_log.fact_json")
575
+ };
576
+ };
577
+ const readLogicalPayload = (sql, row) => {
578
+ if (row.namespace !== effectDurableAgentNamespace) return JSON.parse(row.fact_json);
579
+ if (row.type === commandAdmittedEventType) return { command: readCommandInputPayload(sql, readCommandIdFromFact(row)) };
580
+ if (contextMessageEventTypes.has(row.type)) return JSON.parse(readMessagePayloadByContextSeq(sql, row.seq).payload_json);
581
+ if (row.type === summaryCreatedEventType) return {
582
+ ...JSON.parse(row.fact_json),
583
+ summary: readSummaryPayloadFromCreatedSeq(sql, row.seq)
584
+ };
585
+ return JSON.parse(row.fact_json);
586
+ };
587
+ const eventLogFactPayload = (namespace, type, payload) => {
588
+ if (namespace !== effectDurableAgentNamespace || typeof payload !== "object" || payload === null) return payload;
589
+ const record = payload;
590
+ switch (type) {
591
+ case commandAdmittedEventType: return { command: commandFact(record.command) };
592
+ case systemMessageCommittedEventType: return pick(record, ["messageId"]);
593
+ case userMessageCommittedEventType: return pick(record, ["commandId", "messageId"]);
594
+ case steeringMessageQueuedEventType: return pick(record, [
595
+ "commandId",
596
+ "messageId",
597
+ "runId"
598
+ ]);
599
+ case assistantMessageCommittedEventType:
600
+ case assistantMessageImportedEventType: return pick(record, [
601
+ "messageId",
602
+ "runId",
603
+ "turnId",
604
+ "inferenceId"
605
+ ]);
606
+ case assistantPartialCommittedEventType: return pick(record, [
607
+ "messageId",
608
+ "runId",
609
+ "turnId",
610
+ "inferenceId",
611
+ "reason"
612
+ ]);
613
+ case summaryCreatedEventType: return pick(record, [
614
+ "compactionId",
615
+ "summaryId",
616
+ "sourceFromSeq",
617
+ "sourceToSeq"
618
+ ]);
619
+ default: return payload;
620
+ }
621
+ };
622
+ const commandFact = (command) => {
623
+ if (typeof command !== "object" || command === null) return command;
624
+ const record = command;
625
+ return record._tag === "SubmitMessage" ? pick(record, [
626
+ "_tag",
627
+ "commandId",
628
+ "disposition",
629
+ "idempotencyKey"
630
+ ]) : command;
631
+ };
632
+ const pick = (record, keys) => {
633
+ const out = {};
634
+ for (const key of keys) if (record[key] !== void 0) out[key] = record[key];
635
+ return out;
636
+ };
637
+ const readCommandIdFromFact = (row) => {
638
+ const commandId = JSON.parse(row.fact_json).command?.commandId;
639
+ if (typeof commandId !== "string") throw new Error(`CommandAdmitted fact at seq ${row.seq} missing command.commandId`);
640
+ return commandId;
641
+ };
642
+ const readCommandInputPayload = (sql, commandId) => {
643
+ const row = sql.exec(`
644
+ SELECT payload_json
645
+ FROM _eda_command_inputs
646
+ WHERE command_id = ?
647
+ `, commandId).toArray()[0];
648
+ if (row === void 0) throw new Error(`Command input ${commandId} missing from _eda_command_inputs`);
649
+ return JSON.parse(row.payload_json);
650
+ };
651
+ const readSummaryPayloadOptional = (sql, summaryId) => {
652
+ const row = sql.exec(`
653
+ SELECT payload_json
654
+ FROM _eda_context_summaries
655
+ WHERE summary_id = ?
656
+ `, summaryId).toArray()[0];
657
+ return row === void 0 ? void 0 : Schema.decodeUnknownSync(CompactionSummaryArtifact)(JSON.parse(row.payload_json));
658
+ };
659
+ const readSummaryPayloadFromCreatedSeq = (sql, createdSeq) => {
660
+ const row = sql.exec(`
661
+ SELECT payload_json
662
+ FROM _eda_context_summaries
663
+ WHERE created_seq = ?
664
+ `, createdSeq).toArray()[0];
665
+ if (row === void 0) throw new Error(`SummaryCreated at seq ${createdSeq} missing from _eda_context_summaries`);
666
+ return JSON.parse(row.payload_json);
667
+ };
668
+ /**
669
+ * Maintain bounded storage projections in the same transaction as the event.
670
+ *
671
+ * These rows are durability aids, not a second source of lifecycle truth: event
672
+ * order still comes from `_eda_event_log.seq`, while projections make command
673
+ * lookup and body-sidecar hydration bounded.
674
+ */
675
+ const applySynchronousProjections = (sql, entry, seq, factJson) => {
676
+ const { event } = entry;
677
+ if (event.namespace !== effectDurableAgentNamespace) return;
678
+ writeCommandProjection(sql, event, seq, factJson);
679
+ writeCommandInputProjection(sql, event, seq);
680
+ writeMessageProjection(sql, event, seq);
681
+ writeSummaryProjection(sql, event, seq);
682
+ writeContextRebaseProjection(sql, event, seq);
683
+ };
684
+ const writeCommandProjection = (sql, event, seq, factJson) => {
685
+ const payload = Schema.decodeUnknownSync(ProjectionPayload)(event.payload);
686
+ switch (event.type) {
687
+ case commandAdmittedEventType: {
688
+ const command = Schema.decodeUnknownSync(AdmittedCommandProjection)(payload.command);
689
+ const commandId = command.commandId;
690
+ if (commandId === void 0) throw new Error("CommandAdmitted missing command.commandId");
691
+ sql.exec(`
692
+ INSERT INTO _eda_command_state (
693
+ command_id,
694
+ admitted_seq,
695
+ status,
696
+ idempotency_key,
697
+ payload_json
698
+ ) VALUES (?, ?, ?, ?, ?)
699
+ ON CONFLICT(command_id) DO UPDATE SET
700
+ admitted_seq = excluded.admitted_seq,
701
+ status = excluded.status,
702
+ idempotency_key = excluded.idempotency_key,
703
+ payload_json = excluded.payload_json
704
+ `, commandId, seq, "admitted", command.idempotencyKey ?? null, factJson);
705
+ return;
706
+ }
707
+ case commandStartedEventType:
708
+ writeCommandStatus(sql, payload.commandId, seq, "started", factJson);
709
+ return;
710
+ case commandCompletedEventType:
711
+ writeCommandStatus(sql, payload.commandId, seq, "completed", factJson);
712
+ return;
713
+ case commandFailedEventType:
714
+ writeCommandStatus(sql, payload.commandId, seq, "failed", factJson);
715
+ return;
716
+ case commandCancelledEventType:
717
+ writeCommandStatus(sql, payload.commandId, seq, "cancelled", factJson);
718
+ return;
719
+ default: return;
720
+ }
721
+ };
722
+ const writeCommandStatus = (sql, commandId, seq, status, payloadJson) => {
723
+ if (commandId === void 0) throw new Error(`${status} command event missing commandId`);
724
+ sql.exec(`
725
+ INSERT INTO _eda_command_state (
726
+ command_id,
727
+ admitted_seq,
728
+ status,
729
+ idempotency_key,
730
+ payload_json
731
+ ) VALUES (?, ?, ?, NULL, ?)
732
+ ON CONFLICT(command_id) DO UPDATE SET
733
+ status = excluded.status,
734
+ payload_json = excluded.payload_json
735
+ `, commandId, seq, status, payloadJson);
736
+ };
737
+ const writeCommandInputProjection = (sql, event, seq) => {
738
+ if (event.type !== commandAdmittedEventType) return;
739
+ const command = Schema.encodeSync(DurableEventEnvelope)(event).payload.command;
740
+ const commandId = command?.commandId;
741
+ if (commandId === void 0) throw new Error("CommandAdmitted missing command.commandId");
742
+ sql.exec(`
743
+ INSERT INTO _eda_command_inputs (
744
+ command_id,
745
+ admitted_seq,
746
+ payload_json
747
+ ) VALUES (?, ?, ?)
748
+ ON CONFLICT(command_id) DO UPDATE SET
749
+ admitted_seq = excluded.admitted_seq,
750
+ payload_json = excluded.payload_json
751
+ `, commandId, seq, stringifyJsonForColumn(command, "_eda_command_inputs.payload_json"));
752
+ };
753
+ const contextMessageEventTypes = new Set([
754
+ systemMessageCommittedEventType,
755
+ userMessageCommittedEventType,
756
+ steeringMessageQueuedEventType,
757
+ assistantMessageCommittedEventType,
758
+ assistantMessageImportedEventType,
759
+ assistantPartialCommittedEventType
760
+ ]);
761
+ /** Persist model-visible message bodies with the seq that exposed them to context. */
762
+ const writeMessageProjection = (sql, event, seq) => {
763
+ if (!contextMessageEventTypes.has(event.type)) return;
764
+ const encoded = Schema.encodeSync(DurableEventEnvelope)(event);
765
+ const messageId = encoded.payload.messageId;
766
+ if (messageId === void 0) throw new Error(`${event.type} missing messageId`);
767
+ sql.exec(`
768
+ INSERT INTO _eda_context_messages (
769
+ message_id,
770
+ context_seq,
771
+ payload_json
772
+ ) VALUES (?, ?, ?)
773
+ ON CONFLICT(message_id) DO UPDATE SET
774
+ context_seq = excluded.context_seq,
775
+ payload_json = excluded.payload_json
776
+ `, messageId, seq, stringifyJsonForColumn(encoded.payload, "_eda_context_messages.payload_json"));
777
+ };
778
+ /**
779
+ * Persist a cumulative summary artifact without making it the active context.
780
+ *
781
+ * `ContextRebased` selects summaries in the framework reduced-state checkpoint;
782
+ * failed compactions can leave orphaned summaries without changing future context.
783
+ */
784
+ const writeSummaryProjection = (sql, event, seq) => {
785
+ if (event.type !== summaryCreatedEventType) return;
786
+ const payload = event.payload;
787
+ const summaryId = payload.summaryId;
788
+ if (summaryId === void 0) throw new Error("SummaryCreated missing summaryId");
789
+ if (payload.summary === void 0) throw new Error("SummaryCreated requires summary payload");
790
+ const summary = Schema.decodeUnknownSync(CompactionSummaryArtifact)(payload.summary);
791
+ if (summary.summaryId !== summaryId) throw new Error("SummaryCreated summaryId does not match summary payload");
792
+ if (summary.compactionId !== payload.compactionId) throw new Error("SummaryCreated compactionId does not match summary payload");
793
+ sql.exec(`
794
+ INSERT INTO _eda_context_summaries (
795
+ summary_id,
796
+ created_seq,
797
+ payload_json
798
+ ) VALUES (?, ?, ?)
799
+ ON CONFLICT(summary_id) DO UPDATE SET
800
+ created_seq = excluded.created_seq,
801
+ payload_json = excluded.payload_json
802
+ `, summaryId, seq, stringifyJsonForColumn(summary, "_eda_context_summaries.payload_json"));
803
+ };
804
+ /** Validate that `ContextRebased` points at a committed summary sidecar. */
805
+ const writeContextRebaseProjection = (sql, event, _seq) => {
806
+ if (event.type !== contextRebasedEventType) return;
807
+ const payload = event.payload;
808
+ if (payload.summaryId === void 0) throw new Error("ContextRebased missing summaryId");
809
+ const summary = readSummaryPayloadOptional(sql, SummaryId.make(String(payload.summaryId)));
810
+ if (summary === void 0) throw new Error(`ContextRebased references unknown summaryId ${String(payload.summaryId)}`);
811
+ if (summary.compactionId !== payload.compactionId) throw new Error("ContextRebased summary compactionId does not match event payload");
812
+ if (summary.retainedFromContextSeq !== payload.retainedFromContextSeq) throw new Error("ContextRebased retained cursor does not match summary payload");
813
+ };
814
+ /**
815
+ * Lookup prior command admission through `_eda_command_state` instead of genesis replay.
816
+ *
817
+ * Idempotency key wins over explicit command id so caller-owned retry keys remain
818
+ * the stable dedupe surface for external ingress.
819
+ */
820
+ const findCommandAdmissionRow = (sql, commandId, idempotencyKey) => {
821
+ if (idempotencyKey !== void 0) {
822
+ const row = sql.exec(`
823
+ SELECT admitted_seq
824
+ FROM _eda_command_state
825
+ WHERE idempotency_key = ?
826
+ `, idempotencyKey).toArray()[0];
827
+ if (row !== void 0) return row;
828
+ }
829
+ if (commandId === void 0) return;
830
+ return sql.exec(`
831
+ SELECT admitted_seq
832
+ FROM _eda_command_state
833
+ WHERE command_id = ?
834
+ `, commandId).toArray()[0];
835
+ };
836
+ /** Serialize and size-check one JSON column before it reaches DO SQLite. */
837
+ const stringifyJsonForColumn = (value, column) => {
838
+ const json = JSON.stringify(value);
839
+ if (json === void 0) throw new Error(`${column} value is not JSON serializable`);
840
+ const bytes = jsonEncoder.encode(json).byteLength;
841
+ if (bytes > 15e5) throw new Error(`${column} serialized JSON is ${bytes} bytes; hard cap is ${durableObjectSerializedJsonHardCapBytes} bytes`);
842
+ return json;
843
+ };
844
+ const durableStoreError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
845
+ //#endregion
846
+ //#region src/runtime/runtime-layer.ts
847
+ /** Compose the platform-neutral runtime with Cloudflare persistence and lifecycle adapters. */
848
+ const makeEDADurableObjectRuntimeLayer = ({ config, compactionExecutorLayer, compactionPolicyLayer, keepAlive, modelResolverLayer, promptProjectorLayer, reducers, runSchedulerLayer, sessionEventObserverLayer, sessionId, sinks, storage, toolkit, toolRegistry, tracer }) => {
849
+ const lifecycle = keepAlive ?? new DurableObjectKeepAlive(storage);
850
+ return makeEDARuntimeLayer({
851
+ config,
852
+ compactionExecutorLayer,
853
+ compactionPolicyLayer,
854
+ keepAliveLayer: EDAKeepAlive.FromAcquire(() => lifecycle.acquire()),
855
+ modelResolverLayer,
856
+ promptProjectorLayer,
857
+ runSchedulerLayer,
858
+ runSchedulingWakeupLayer: Layer.succeed(RunSchedulingWakeup, { setPending: (pending) => Effect.tryPromise({
859
+ try: () => lifecycle.setRunSchedulingPending(pending),
860
+ catch: (error) => new EDASessionStoreError({ message: `Persisting run scheduling wakeup: ${String(error)}` })
861
+ }) }),
862
+ reducerRegistryLayer: EDAReducerRegistry.Live(reducers ?? []),
863
+ sessionEventObserverLayer,
864
+ sessionId,
865
+ sessionStoreLayer: DurableObjectSessionStore.layer({
866
+ sessionId,
867
+ storage
868
+ }),
869
+ sinkCheckpointStoreLayer: DurableObjectSinkCheckpointStore.layer(storage),
870
+ sinks,
871
+ toolkit,
872
+ toolRegistry,
873
+ tracer
874
+ });
875
+ };
876
+ //#endregion
877
+ //#region src/runtime/session-runtime.ts
878
+ /** Owns the disposable ManagedRuntime and nothing about WebSocket transport. */
879
+ var EDASessionRuntime = class {
880
+ options;
881
+ keepAlive;
882
+ state;
883
+ building;
884
+ constructor(options) {
885
+ this.options = options;
886
+ this.keepAlive = options.keepAlive ?? new DurableObjectKeepAlive(options.storage, options.background);
887
+ }
888
+ static migrate = (storage) => Effect.gen(function* () {
889
+ yield* DurableObjectSessionStore.migrate(storage);
890
+ yield* DurableObjectSinkCheckpointStore.migrate(storage);
891
+ });
892
+ isReady(sessionId) {
893
+ return this.state?.sessionId === sessionId;
894
+ }
895
+ async prepare(sessionId) {
896
+ await this.get(sessionId);
897
+ }
898
+ async run(sessionId, operation) {
899
+ return await (await this.get(sessionId)).runtime.runPromise(Effect.gen(function* () {
900
+ return yield* operation(yield* EDARuntime);
901
+ }));
902
+ }
903
+ async alarm(sessionId) {
904
+ const resolved = sessionId ?? this.state?.sessionId;
905
+ if (resolved !== void 0) await this.run(resolved, (runtime) => runtime.retryRunSchedulingDelivery());
906
+ await this.keepAlive.alarm();
907
+ }
908
+ async destroy() {
909
+ await this.keepAlive.shutdown();
910
+ await this.dispose();
911
+ this.state = void 0;
912
+ this.building = void 0;
913
+ }
914
+ async dispose() {
915
+ await this.state?.runtime.dispose();
916
+ }
917
+ async get(sessionId) {
918
+ if (this.state !== void 0) {
919
+ if (this.state.sessionId !== sessionId) throw new Error(`EDA session runtime is scoped to session ${this.state.sessionId}; received ${sessionId}`);
920
+ return this.state;
921
+ }
922
+ this.keepAlive.restart();
923
+ const building = this.building ?? this.build(sessionId);
924
+ this.building = building;
925
+ try {
926
+ return await building;
927
+ } catch (error) {
928
+ if (this.building === building) this.building = void 0;
929
+ throw error;
930
+ }
931
+ }
932
+ async build(sessionId) {
933
+ const runtime = ManagedRuntime.make(makeEDADurableObjectRuntimeLayer({
934
+ config: this.options.config,
935
+ compactionExecutorLayer: this.options.compactionExecutorLayer,
936
+ compactionPolicyLayer: this.options.compactionPolicyLayer,
937
+ keepAlive: this.keepAlive,
938
+ modelResolverLayer: this.options.modelResolverLayer,
939
+ promptProjectorLayer: this.options.promptProjectorLayer,
940
+ reducers: this.options.reducers,
941
+ runSchedulerLayer: this.options.runSchedulerLayer,
942
+ sessionEventObserverLayer: this.options.sessionEventObserverLayer,
943
+ sessionId,
944
+ sinks: this.options.sinks,
945
+ storage: this.options.storage,
946
+ toolkit: resolveFactory(this.options.toolkit, sessionId),
947
+ toolRegistry: resolveFactory(this.options.toolRegistry, sessionId),
948
+ tracer: resolveFactory(this.options.tracer, sessionId)
949
+ }));
950
+ const state = {
951
+ runtime,
952
+ sessionId
953
+ };
954
+ try {
955
+ await runtime.context();
956
+ this.state = state;
957
+ return state;
958
+ } catch (error) {
959
+ await runtime.dispose().catch(() => void 0);
960
+ throw error;
961
+ }
962
+ }
963
+ };
964
+ const resolveFactory = (value, sessionId) => typeof value === "function" ? value({ sessionId }) : value;
965
+ //#endregion
966
+ //#region src/websocket/projection.ts
967
+ /** Stable non-empty identity persisted with an app WebSocket projection. */
968
+ const EDAWebSocketProjectionId = Schema.NonEmptyString.pipe(Schema.brand("EDAWebSocketProjectionId"));
969
+ //#endregion
970
+ //#region src/websocket/attachment.ts
971
+ /** Versioned state required to resume strict delivery after Durable Object hibernation. */
972
+ const EDAWebSocketAttachment = Schema.Struct({
973
+ kind: Schema.Literal("eda-events-v2"),
974
+ sessionId: SessionId,
975
+ subscriberId: SubscriberId,
976
+ trace: EDATraceMetadata,
977
+ delivery: EDAWebSocketDeliveryCheckpoint,
978
+ projection: Schema.optionalKey(Schema.Struct({
979
+ id: EDAWebSocketProjectionId,
980
+ state: Schema.Unknown
981
+ }))
982
+ });
983
+ /** Decode and classify an attachment read from Cloudflare's structured-clone boundary. */
984
+ const decodeWebSocketAttachment = async (input) => {
985
+ if (input === null || input === void 0) return { _tag: "Missing" };
986
+ try {
987
+ return {
988
+ _tag: "Decoded",
989
+ attachment: await Effect.runPromise(Schema.decodeUnknownEffect(EDAWebSocketAttachment)(input))
990
+ };
991
+ } catch {
992
+ return { _tag: "Malformed" };
993
+ }
994
+ };
995
+ /** Encode before serialization so schema transformations cannot leak into attachments. */
996
+ const encodeWebSocketAttachment = (attachment) => Schema.encodeSync(EDAWebSocketAttachment)(attachment);
997
+ //#endregion
998
+ //#region src/websocket/connection-manager.ts
999
+ /**
1000
+ * Owns Cloudflare WebSockets, hibernation attachments, and interpretation of
1001
+ * the platform-neutral delivery state machine.
1002
+ */
1003
+ var EDAWebSocketConnectionManager = class {
1004
+ options;
1005
+ observerLayer = SessionEventObserver.FromHandler((event) => Effect.tryPromise(() => this.fanoutPublishedEvent(event)).pipe(Effect.catchCause((cause) => Effect.logError("EDA WebSocket event observer recovered from a host failure", { cause }))));
1006
+ sockets = /* @__PURE__ */ new Map();
1007
+ constructor(options) {
1008
+ this.options = options;
1009
+ }
1010
+ resolveProjection(id) {
1011
+ if (id === void 0) return void 0;
1012
+ if (this.options.webSocketProjection?.id === id) return this.options.webSocketProjection;
1013
+ }
1014
+ async accept(input) {
1015
+ const subscriberId = SubscriberId.make(crypto.randomUUID());
1016
+ const delivery = makeWebSocketDeliveryState({
1017
+ subscriberId,
1018
+ resumeSeq: input.afterSeq,
1019
+ policy: defaultEDAWebSocketFlowControl
1020
+ });
1021
+ const state = {
1022
+ sessionId: input.sessionId,
1023
+ subscriberId,
1024
+ trace: input.trace,
1025
+ delivery,
1026
+ catchUpDeferred: false,
1027
+ head: input.afterSeq,
1028
+ queue: Promise.resolve(),
1029
+ ...input.projectionState === void 0 ? {} : { projectionState: input.projectionState }
1030
+ };
1031
+ this.sockets.set(input.webSocket, state);
1032
+ if (!this.persist(input.webSocket, state)) return;
1033
+ if (!await this.send(input.webSocket, state, [deliveryHelloFrame(delivery)])) return;
1034
+ if (!await this.prepare(input.webSocket, state)) return;
1035
+ await this.enqueue(input.webSocket, state, async () => {
1036
+ await this.readAndDeliver(input.webSocket, state, input.afterSeq);
1037
+ });
1038
+ }
1039
+ async message(webSocket, message) {
1040
+ if (typeof message !== "string") {
1041
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "binary");
1042
+ return;
1043
+ }
1044
+ if (message === EDA_WEB_SOCKET_PING_MESSAGE) {
1045
+ try {
1046
+ webSocket.send(EDA_WEB_SOCKET_PONG_MESSAGE);
1047
+ } catch {
1048
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "send-failed");
1049
+ }
1050
+ return;
1051
+ }
1052
+ const state = await this.restore(webSocket);
1053
+ if (state === void 0) {
1054
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1055
+ return;
1056
+ }
1057
+ const frame = await Effect.runPromise(this.decodeClientMessage(message, state).pipe(Effect.option));
1058
+ if (frame._tag === "None") {
1059
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1060
+ return;
1061
+ }
1062
+ const clientFrame = frame.value;
1063
+ if (clientFrame._tag === "ping") return;
1064
+ if (!await this.prepare(webSocket, state)) return;
1065
+ await this.enqueue(webSocket, state, async () => {
1066
+ await this.interpret(webSocket, state, onClientAck(state.delivery, clientFrame, Date.now()));
1067
+ });
1068
+ }
1069
+ closed(webSocket) {
1070
+ this.sockets.delete(webSocket);
1071
+ }
1072
+ async flushDeferredCatchUp(sessionId) {
1073
+ const deliveries = [];
1074
+ for (const [webSocket, state] of this.sockets) {
1075
+ if (state.sessionId !== sessionId || !state.catchUpDeferred) continue;
1076
+ deliveries.push(this.enqueue(webSocket, state, async () => {
1077
+ if (!state.catchUpDeferred) return;
1078
+ state.catchUpDeferred = false;
1079
+ await this.readAndDeliver(webSocket, state, state.delivery.sentDurableThroughSeq);
1080
+ }));
1081
+ }
1082
+ await Promise.all(deliveries);
1083
+ }
1084
+ async fanoutPublishedEvent(event) {
1085
+ const webSockets = new Set([...this.options.getWebSockets?.() ?? [], ...this.sockets.keys()]);
1086
+ const deliveries = [];
1087
+ for (const webSocket of webSockets) {
1088
+ const state = await this.restore(webSocket);
1089
+ if (state === void 0) continue;
1090
+ deliveries.push(this.enqueue(webSocket, state, async () => {
1091
+ if (event.event.durability !== "durable") {
1092
+ await this.interpret(webSocket, state, onEphemeralEvent(state.delivery, event, Date.now()));
1093
+ return;
1094
+ }
1095
+ state.head = SequenceNumber.make(Math.max(state.head, event.position.seq));
1096
+ if (event.position.seq <= state.delivery.sentDurableThroughSeq) return;
1097
+ if (event.position.seq === state.delivery.sentDurableThroughSeq + 1) {
1098
+ await this.interpret(webSocket, state, onDurableEvents(state.delivery, [event], state.head, Date.now()));
1099
+ return;
1100
+ }
1101
+ if (!this.options.isSessionReady(state.sessionId)) {
1102
+ state.catchUpDeferred = true;
1103
+ return;
1104
+ }
1105
+ await this.readAndDeliver(webSocket, state, state.delivery.sentDurableThroughSeq);
1106
+ }));
1107
+ }
1108
+ await Promise.all(deliveries);
1109
+ }
1110
+ async restore(webSocket) {
1111
+ const existing = this.sockets.get(webSocket);
1112
+ if (existing !== void 0) return existing;
1113
+ const decoded = await decodeWebSocketAttachment(webSocket.deserializeAttachment());
1114
+ if (decoded._tag !== "Decoded") {
1115
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1116
+ return;
1117
+ }
1118
+ const attachment = decoded.attachment;
1119
+ const projectionState = attachment.projection === void 0 ? void 0 : await this.decodeProjectionState(attachment.projection.id, attachment.projection.state);
1120
+ if (attachment.projection !== void 0 && projectionState === void 0) {
1121
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1122
+ return;
1123
+ }
1124
+ const state = {
1125
+ sessionId: attachment.sessionId,
1126
+ subscriberId: attachment.subscriberId,
1127
+ trace: attachment.trace,
1128
+ delivery: restoreWebSocketDeliveryState({
1129
+ subscriberId: attachment.subscriberId,
1130
+ policy: defaultEDAWebSocketFlowControl,
1131
+ checkpoint: attachment.delivery
1132
+ }),
1133
+ catchUpDeferred: false,
1134
+ head: attachment.delivery.sentDurableThroughSeq,
1135
+ queue: Promise.resolve(),
1136
+ ...projectionState === void 0 ? {} : { projectionState }
1137
+ };
1138
+ this.sockets.set(webSocket, state);
1139
+ return state;
1140
+ }
1141
+ async prepare(webSocket, state) {
1142
+ try {
1143
+ await this.options.prepareSession(state.sessionId);
1144
+ return this.sockets.get(webSocket) === state;
1145
+ } catch (error) {
1146
+ await Effect.runPromise(Effect.logError("EDA WebSocket session preparation failed", {
1147
+ error,
1148
+ subscriberId: state.subscriberId
1149
+ }));
1150
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
1151
+ return false;
1152
+ }
1153
+ }
1154
+ async interpret(webSocket, state, result) {
1155
+ state.delivery = result.state;
1156
+ for (const action of result.actions) switch (action._tag) {
1157
+ case "Persist":
1158
+ if (!this.persist(webSocket, state)) return false;
1159
+ break;
1160
+ case "Send":
1161
+ if (!await this.send(webSocket, state, action.frames)) return false;
1162
+ break;
1163
+ case "ReadEventPage":
1164
+ if (!await this.readAndDeliver(webSocket, state, action.afterSeq)) return false;
1165
+ break;
1166
+ case "Close":
1167
+ this.close(webSocket, action.code, action.reason);
1168
+ return false;
1169
+ }
1170
+ return true;
1171
+ }
1172
+ async readAndDeliver(webSocket, state, afterSeq) {
1173
+ const page = await this.options.readEventPage({
1174
+ afterSeq,
1175
+ limit: state.delivery.policy.maxInFlightFrames * state.delivery.policy.maxFrameEvents,
1176
+ sessionId: state.sessionId,
1177
+ subscriberId: state.subscriberId,
1178
+ trace: state.trace
1179
+ });
1180
+ state.head = SequenceNumber.make(Math.max(state.head, page.head));
1181
+ if (page.events.length === 0) return true;
1182
+ return await this.interpret(webSocket, state, onDurableEvents(state.delivery, page.events, state.head, Date.now()));
1183
+ }
1184
+ persist(webSocket, state) {
1185
+ try {
1186
+ const projection = state.projectionState === void 0 ? void 0 : this.options.webSocketProjection;
1187
+ if (state.projectionState !== void 0 && projection === void 0) {
1188
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1189
+ return false;
1190
+ }
1191
+ const attachment = EDAWebSocketAttachment.make({
1192
+ kind: "eda-events-v2",
1193
+ sessionId: state.sessionId,
1194
+ subscriberId: state.subscriberId,
1195
+ trace: state.trace,
1196
+ delivery: checkpointWebSocketDeliveryState(state.delivery),
1197
+ ...state.projectionState === void 0 || projection === void 0 ? {} : { projection: {
1198
+ id: projection.id,
1199
+ state: projection.encodeState(state.projectionState)
1200
+ } }
1201
+ });
1202
+ webSocket.serializeAttachment(encodeWebSocketAttachment(attachment));
1203
+ return true;
1204
+ } catch {
1205
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "attachment-failed");
1206
+ return false;
1207
+ }
1208
+ }
1209
+ send(webSocket, state, frames) {
1210
+ return this.sendPrepared(webSocket, state, frames);
1211
+ }
1212
+ async sendPrepared(webSocket, state, frames) {
1213
+ try {
1214
+ const encodedFrames = [];
1215
+ const followUpActions = [];
1216
+ for (const frame of frames) {
1217
+ const projected = state.projectionState === void 0 ? void 0 : this.options.webSocketProjection?.encodeServerFrame(frame, state.projectionState);
1218
+ if (state.projectionState !== void 0 && projected === void 0) {
1219
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1220
+ return false;
1221
+ }
1222
+ if (projected?._tag === "SuppressAndAck") {
1223
+ if (frame._tag !== "events") {
1224
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1225
+ return false;
1226
+ }
1227
+ state.projectionState = projected.state;
1228
+ const acknowledged = onHostSuppressedFrame(state.delivery, {
1229
+ durableThroughSeq: frame.durableThroughSeq,
1230
+ frameId: frame.frameId
1231
+ }, Date.now());
1232
+ state.delivery = acknowledged.state;
1233
+ followUpActions.push(...acknowledged.actions.filter((action) => action._tag !== "Persist"));
1234
+ continue;
1235
+ }
1236
+ if (projected !== void 0) state.projectionState = projected.state;
1237
+ const encoded = projected?._tag === "Send" ? projected.frame : encodeEDAWebSocketServerFrame(frame, this.options.webSocketProtocol);
1238
+ if (new TextEncoder().encode(encoded).byteLength > state.delivery.policy.maxFrameBytes) {
1239
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "frame-too-large");
1240
+ return false;
1241
+ }
1242
+ encodedFrames.push(encoded);
1243
+ }
1244
+ if (state.projectionState !== void 0 && !this.persist(webSocket, state)) return false;
1245
+ for (const encoded of encodedFrames) webSocket.send(encoded);
1246
+ if (followUpActions.length > 0) return await this.interpret(webSocket, state, {
1247
+ state: state.delivery,
1248
+ actions: coalesceFollowUpActions(followUpActions)
1249
+ });
1250
+ return true;
1251
+ } catch {
1252
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "send-failed");
1253
+ return false;
1254
+ }
1255
+ }
1256
+ enqueue(webSocket, state, operation) {
1257
+ const run = async () => {
1258
+ try {
1259
+ await operation();
1260
+ } catch (error) {
1261
+ await Effect.runPromise(Effect.logError("EDA WebSocket operation failed", {
1262
+ error,
1263
+ subscriberId: state.subscriberId
1264
+ }));
1265
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
1266
+ }
1267
+ };
1268
+ const next = state.queue.then(run);
1269
+ state.queue = next;
1270
+ return next;
1271
+ }
1272
+ close(webSocket, code, reason) {
1273
+ this.sockets.delete(webSocket);
1274
+ try {
1275
+ webSocket.close(code, reason);
1276
+ } catch {}
1277
+ }
1278
+ decodeClientMessage(message, state) {
1279
+ return state.projectionState === void 0 ? decodeEDAWebSocketClientMessage(message) : this.options.webSocketProjection?.decodeClientMessage(message) ?? Effect.fail("protocol");
1280
+ }
1281
+ async decodeProjectionState(id, encoded) {
1282
+ const projection = this.resolveProjection(id);
1283
+ if (projection === void 0) return void 0;
1284
+ const decoded = await Effect.runPromise(projection.decodeState(encoded).pipe(Effect.option));
1285
+ return decoded._tag === "Some" ? decoded.value : void 0;
1286
+ }
1287
+ };
1288
+ const coalesceFollowUpActions = (actions) => {
1289
+ let readAfterSeq;
1290
+ const retained = [];
1291
+ for (const action of actions) {
1292
+ if (action._tag !== "ReadEventPage") {
1293
+ retained.push(action);
1294
+ continue;
1295
+ }
1296
+ readAfterSeq = SequenceNumber.make(Math.max(readAfterSeq ?? 0, action.afterSeq));
1297
+ }
1298
+ return readAfterSeq === void 0 ? retained : [...retained, {
1299
+ _tag: "ReadEventPage",
1300
+ afterSeq: readAfterSeq
1301
+ }];
1302
+ };
1303
+ //#endregion
1304
+ //#region src/session-controller.ts
1305
+ /**
1306
+ * Public application-facing use cases for one EDA session.
1307
+ *
1308
+ * Runtime lifecycle belongs to EDASessionRuntime; WebSocket transport belongs
1309
+ * to EDAWebSocketConnectionManager. This class only coordinates them.
1310
+ */
1311
+ var EDASessionController = class {
1312
+ runtime;
1313
+ webSockets;
1314
+ constructor(options) {
1315
+ this.webSockets = new EDAWebSocketConnectionManager({
1316
+ getWebSockets: options.getWebSockets,
1317
+ isSessionReady: (sessionId) => this.runtime.isReady(sessionId),
1318
+ prepareSession: (sessionId) => this.prepareSession(sessionId),
1319
+ readEventPage: async ({ afterSeq, limit, sessionId, subscriberId, trace }) => await this.runtime.run(sessionId, (eda) => eda.readEventPage(afterSeq, limit).pipe(Effect.withSpan("agent.events.page", hostSpanOptions(trace, {
1320
+ "eda.session.id": sessionId,
1321
+ "eda.subscriber.id": subscriberId,
1322
+ "eda.seq.after": afterSeq
1323
+ })))),
1324
+ webSocketProjection: options.webSocketProjection,
1325
+ webSocketProtocol: options.webSocketProtocol
1326
+ });
1327
+ this.runtime = new EDASessionRuntime({
1328
+ ...options,
1329
+ sessionEventObserverLayer: this.webSockets.observerLayer
1330
+ });
1331
+ }
1332
+ static migrate = EDASessionRuntime.migrate;
1333
+ /** Trusted reconciliation read; never submits or grants replacement work. */
1334
+ runRequestOutcome(input) {
1335
+ return this.run(input.sessionId, (eda) => eda.runRequestOutcome(input.requestId));
1336
+ }
1337
+ /** Trusted scheduler ingress. Hosts must authorize callers before invoking this method. */
1338
+ grantRun(input) {
1339
+ const parent = input.trace?.parent;
1340
+ return this.run(input.sessionId, (eda) => {
1341
+ const operation = eda.grantRun(input.command);
1342
+ return parent === void 0 || parent === null ? operation : operation.pipe(Effect.withParentSpan(toEdaExternalSpan(parent)));
1343
+ });
1344
+ }
1345
+ submit(input) {
1346
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1347
+ return this.run(input.sessionId, (eda) => eda.submit(input.command).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
1348
+ "eda.session.id": input.sessionId,
1349
+ "eda.command.kind": input.command._tag,
1350
+ "eda.command.id": input.command.commandId
1351
+ }))));
1352
+ }
1353
+ submitBatch(input) {
1354
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1355
+ return this.run(input.sessionId, (eda) => eda.submit(input.items).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
1356
+ "eda.session.id": input.sessionId,
1357
+ "eda.command.submittable_count": input.items.length
1358
+ }))));
1359
+ }
1360
+ submitAndBlock(input) {
1361
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1362
+ return this.run(input.sessionId, (eda) => eda.submitAndBlock(input.command).pipe(Effect.withSpan("agent.command.submit.wait", hostSpanOptions(trace, {
1363
+ "eda.session.id": input.sessionId,
1364
+ "eda.command.kind": input.command._tag,
1365
+ "eda.command.id": input.command.commandId
1366
+ }))));
1367
+ }
1368
+ blockOnCommand(input) {
1369
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1370
+ return this.run(input.sessionId, (eda) => eda.blockOnCommand(input.commandId, input.afterSeq ?? SequenceNumber.make(0)).pipe(Effect.withSpan("agent.command.wait", hostSpanOptions(trace, {
1371
+ "eda.session.id": input.sessionId,
1372
+ "eda.command.id": input.commandId,
1373
+ "eda.seq.after": input.afterSeq
1374
+ }))));
1375
+ }
1376
+ snapshot(input) {
1377
+ return this.query(input, (eda) => eda.snapshot(), "agent.session.snapshot");
1378
+ }
1379
+ messages(input) {
1380
+ return this.query(input, (eda) => eda.messages(), "agent.messages.list");
1381
+ }
1382
+ acceptEventWebSocket(input) {
1383
+ return this.acceptEventWebSocketUnhosted(input);
1384
+ }
1385
+ async acceptEventWebSocketUnhosted(input) {
1386
+ const prepared = await this.prepareEventWebSocket(input);
1387
+ await this.acceptPreparedEventWebSocket({
1388
+ ...prepared,
1389
+ webSocket: input.webSocket
1390
+ });
1391
+ }
1392
+ async prepareEventWebSocket(input) {
1393
+ const projection = this.webSockets.resolveProjection(input.projectionId);
1394
+ if (input.projectionId !== void 0 && projection === void 0) throw new Error(`Unsupported EDA WebSocket projection: ${input.projectionId}`);
1395
+ const initialized = projection === void 0 ? void 0 : projection.initialize({
1396
+ ...input.afterSeq === void 0 ? {} : { requestedAfterSeq: input.afterSeq },
1397
+ snapshot: await this.snapshot({
1398
+ sessionId: input.sessionId,
1399
+ trace: input.trace
1400
+ })
1401
+ });
1402
+ return {
1403
+ afterSeq: initialized?.afterSeq ?? input.afterSeq ?? SequenceNumber.make(0),
1404
+ sessionId: input.sessionId,
1405
+ trace: input.trace ?? makeRootEDATraceMetadata(),
1406
+ ...initialized === void 0 ? {} : { projectionState: initialized.state }
1407
+ };
1408
+ }
1409
+ acceptPreparedEventWebSocket(input) {
1410
+ return this.webSockets.accept(input);
1411
+ }
1412
+ webSocketMessage(webSocket, message) {
1413
+ return this.webSockets.message(webSocket, message);
1414
+ }
1415
+ webSocketClose(webSocket) {
1416
+ this.webSockets.closed(webSocket);
1417
+ }
1418
+ webSocketError(webSocket) {
1419
+ this.webSockets.closed(webSocket);
1420
+ }
1421
+ async destroy(_input) {
1422
+ await this.runtime.destroy();
1423
+ }
1424
+ async alarm(input) {
1425
+ if (input !== void 0) await this.prepareSession(input.sessionId);
1426
+ await this.runtime.alarm(input?.sessionId);
1427
+ }
1428
+ dispose() {
1429
+ return this.runtime.dispose();
1430
+ }
1431
+ query(input, operation, spanName) {
1432
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1433
+ return this.run(input.sessionId, (eda) => operation(eda).pipe(Effect.withSpan(spanName, hostSpanOptions(trace, { "eda.session.id": input.sessionId }))));
1434
+ }
1435
+ async prepareSession(sessionId) {
1436
+ await this.runtime.prepare(sessionId);
1437
+ await this.webSockets.flushDeferredCatchUp(sessionId);
1438
+ }
1439
+ async run(sessionId, operation) {
1440
+ await this.prepareSession(sessionId);
1441
+ return await this.runtime.run(sessionId, operation);
1442
+ }
1443
+ };
1444
+ const hostSpanOptions = (trace, attributes) => ({
1445
+ kind: "server",
1446
+ attributes: compactSpanAttributes({
1447
+ ...trace.attributes,
1448
+ ...attributes
1449
+ }),
1450
+ links: trace.links.map((link) => ({
1451
+ span: toEdaExternalSpan(link.context),
1452
+ attributes: link.attributes
1453
+ })),
1454
+ ...trace.parent === null ? {} : { parent: toEdaExternalSpan(trace.parent) }
1455
+ });
1456
+ //#endregion
1457
+ export { EDAWebSocketProjectionId as n, makeEDADurableObjectRuntimeLayer as r, EDASessionController as t };
1458
+
1459
+ //# sourceMappingURL=session-controller-C31DhUNp.js.map