effect-durable-agent-cloudflare 0.1.0-alpha.23 → 0.1.0-alpha.25

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,627 @@
1
+ import { S as encodeEdaRpcDurableEvent, a as SessionOutcomeInput, d as SessionTranscriptWindowInput, g as DurableObjectTranscriptStore, h as DurableObjectTranscriptBuilder, i as SessionGrantRunInput, l as SessionTranscriptSummaryInput, m as edaSessionRpc, n as SessionBlockInput, o as SessionScopedInput, r as SessionCommandInput, s as SessionTranscriptBodyInput, t as SessionBatchInput } from "./rpc-spec-BupIkBhI.js";
2
+ import { i as readDurableObjectSummary, m as resolveTranscriptSliceLimits, r as readDurableObjectCommittedEvent, s as defaultTranscriptSliceLimits } from "./durable-object-store-Ca0pvpa_.js";
3
+ import { t as EDASessionController } from "./session-controller-KWMuKhlX.js";
4
+ import * as Effect from "effect/Effect";
5
+ import { durableEventEnvelope } from "effect-durable-agent/types/events/durable";
6
+ import * as Schema from "effect/Schema";
7
+ import { CommandId, RunRequestId, SequenceNumber, SessionId } from "effect-durable-agent/types/core";
8
+ import { EDATraceMetadata, makeEDATraceMetadataFromParent, makeRootEDATraceMetadata, parseEDATraceparent } from "effect-durable-agent/types/tracing";
9
+ import { EDASessionStoreError } from "effect-durable-agent/services/session-store";
10
+ import { defaultTranscriptWindowLimits, emptyTranscriptMappings, makeTranscriptDefinition, resolveTranscriptBody } from "effect-durable-agent/transcript";
11
+ import * as Result from "effect/Result";
12
+ import { DurableObject } from "cloudflare:workers";
13
+ import { EDA_WEB_SOCKET_PING_MESSAGE, EDA_WEB_SOCKET_PONG_MESSAGE } from "effect-durable-agent/websocket";
14
+ //#region src/durable-object-alarm-handler.ts
15
+ /** Longest delay between builder retries. */
16
+ const maxBuilderRetryDelayMs = 36e5;
17
+ /**
18
+ * Handle one platform alarm shared by the runtime and the transcript builder.
19
+ *
20
+ * - The runtime runs for its own wakeup, or for an alarm no owner recorded (one armed by a
21
+ * release before wakeups were recorded). The builder runs for its wakeup even when the runtime
22
+ * failed.
23
+ * - No wakeup is consumed up front: each owner's is cleared only after its work succeeds, so a
24
+ * handler that throws or dies mid-work leaves the platform's retry attributed to every owner it
25
+ * fired for.
26
+ * - A failed builder retries with exponential backoff; its consecutive failures persist with its
27
+ * wakeup, so eviction cannot reset them, and at `maxBuilderFailures` it stops scheduling itself.
28
+ * No failure re-arms the alarm sooner than the backoff, so no object can loop on its alarm.
29
+ * - A failed runtime rethrows without re-arming the platform alarm, so the platform's own retry
30
+ * (backoff and retry cap) applies; otherwise the alarm is pointed at the earliest wakeup left.
31
+ */
32
+ const runEDASessionAlarm = async (participants) => {
33
+ const { alarms } = participants;
34
+ const now = participants.now();
35
+ const due = alarms.due(now);
36
+ let runtimeFailure;
37
+ if (due.has("runtime") || due.size === 0) try {
38
+ await participants.runRuntime();
39
+ alarms.clearThrough("runtime", due.get("runtime") ?? now);
40
+ } catch (error) {
41
+ runtimeFailure = { error };
42
+ }
43
+ const builderWakeup = due.get("transcript");
44
+ if (builderWakeup !== void 0) {
45
+ const runBuilderSlices = participants.runBuilderSlices;
46
+ if (runBuilderSlices === void 0) alarms.clear("transcript");
47
+ else try {
48
+ if (await runBuilderSlices()) alarms.retain("transcript", participants.now());
49
+ else alarms.clearThrough("transcript", builderWakeup);
50
+ } catch (error) {
51
+ const consecutiveFailures = alarms.failures("transcript") + 1;
52
+ const stopped = consecutiveFailures >= participants.maxBuilderFailures;
53
+ if (stopped) alarms.clear("transcript");
54
+ else {
55
+ const delayMs = Math.min(participants.builderRetryDelayMs * 2 ** (consecutiveFailures - 1), maxBuilderRetryDelayMs);
56
+ alarms.retain("transcript", participants.now() + delayMs, consecutiveFailures);
57
+ }
58
+ participants.onBuilderFailure({
59
+ error,
60
+ consecutiveFailures,
61
+ stopped
62
+ });
63
+ }
64
+ }
65
+ if (runtimeFailure !== void 0) throw runtimeFailure.error;
66
+ await alarms.sync();
67
+ };
68
+ //#endregion
69
+ //#region src/durable-object-alarms.ts
70
+ /**
71
+ * Shares one Durable Object alarm between owners that each keep a durable wakeup.
72
+ *
73
+ * Each owner's wakeup is a row in `_eda_alarm_wakeups`, and the platform alarm is always set to the
74
+ * earliest row, so no owner can delete or postpone another's wakeup, and after an eviction the
75
+ * alarm handler still knows which owners it fired for. An owner's view (`owner(...)`) has the
76
+ * same shape as the platform alarm API, so the keep-alive heartbeat uses it unchanged.
77
+ */
78
+ var DurableObjectAlarmScheduler = class {
79
+ storage;
80
+ now;
81
+ #tail = Promise.resolve();
82
+ constructor(storage, now = () => Date.now()) {
83
+ this.storage = storage;
84
+ this.now = now;
85
+ }
86
+ /**
87
+ * One owner's alarm, behaving like the platform alarm restricted to that owner. Like the
88
+ * platform's, it reports no alarm once its time has passed: a wakeup left behind when the
89
+ * platform gave up retrying a failed alarm must not look armed, or its owner would never re-arm.
90
+ */
91
+ owner(owner) {
92
+ return {
93
+ getAlarm: () => {
94
+ const wakeup = this.#wakeup(owner);
95
+ return wakeup === void 0 || wakeup < this.now() ? null : wakeup;
96
+ },
97
+ setAlarm: (scheduledTimeMs) => {
98
+ this.#write(owner, scheduledTimeMs);
99
+ return this.sync();
100
+ },
101
+ deleteAlarm: () => {
102
+ this.#write(owner, void 0);
103
+ return this.sync();
104
+ }
105
+ };
106
+ }
107
+ /**
108
+ * Adopt a platform alarm no owner recorded (one armed before wakeups were recorded) as the
109
+ * runtime's, so the first wakeup another owner requests cannot replace it. Call once at startup,
110
+ * before any owner writes a wakeup.
111
+ */
112
+ async adoptUnrecordedAlarm() {
113
+ if (this.#rows().length > 0) return;
114
+ const armed = await this.storage.getAlarm();
115
+ if (armed !== null && this.#rows().length === 0) this.#write("runtime", armed);
116
+ }
117
+ /**
118
+ * Record `owner`'s wakeup without touching the platform alarm, for use inside `alarm()`: the
119
+ * caller syncs once at the end, or not at all when it rethrows, which leaves the platform's own
120
+ * retry (with its backoff and cap) in force.
121
+ */
122
+ retain(owner, atMs, failures = 0) {
123
+ this.#write(owner, atMs, failures);
124
+ }
125
+ /** Consecutive failures recorded with `owner`'s wakeup; 0 without one. */
126
+ failures(owner) {
127
+ return this.storage.sql.exec("SELECT failures FROM _eda_alarm_wakeups WHERE owner = ?", owner).toArray()[0]?.failures ?? 0;
128
+ }
129
+ /** Remove `owner`'s wakeup whatever its time; call `sync` afterwards. */
130
+ clear(owner) {
131
+ this.#write(owner, void 0);
132
+ }
133
+ /** Wake `owner` no later than `atMs`; an earlier wakeup it already holds is kept. */
134
+ request(owner, atMs) {
135
+ const current = this.#wakeup(owner);
136
+ if (current !== void 0 && current <= atMs) return this.sync();
137
+ this.#write(owner, atMs);
138
+ return this.sync();
139
+ }
140
+ /**
141
+ * Called when the platform alarm fires: the owners it fired for (every wakeup at or before now,
142
+ * and always the earliest one) and the wakeup each held. Nothing is consumed here: each owner
143
+ * `clearThrough`s its wakeup only after its work succeeds, so a handler that throws, or dies
144
+ * mid-work (eviction, deploy, a resource limit), leaves the platform's retry attributed to every
145
+ * owner it fired for. An owner's view already reports a past wakeup as no alarm, so the runtime's
146
+ * keep-alive re-arms during its work as it did with the platform alarm.
147
+ */
148
+ due(nowMs) {
149
+ const rows = this.#rows();
150
+ const earliest = rows.reduce((min, row) => Math.min(min, row.at_ms), Number.POSITIVE_INFINITY);
151
+ const threshold = Math.max(nowMs, earliest);
152
+ const due = /* @__PURE__ */ new Map();
153
+ for (const row of rows) if (row.at_ms <= threshold && isOwner(row.owner)) due.set(row.owner, row.at_ms);
154
+ return due;
155
+ }
156
+ /**
157
+ * Remove `owner`'s wakeup if it is still at or before `atMs`, once the work it fired for has
158
+ * succeeded. A later wakeup the owner re-armed during that work is kept.
159
+ */
160
+ clearThrough(owner, atMs) {
161
+ this.storage.sql.exec("DELETE FROM _eda_alarm_wakeups WHERE owner = ? AND at_ms <= ?", owner, atMs);
162
+ }
163
+ /** Point the platform alarm at the earliest wakeup, or clear it when none remains. */
164
+ sync() {
165
+ this.#tail = this.#tail.catch(() => void 0).then(async () => {
166
+ const earliest = this.storage.sql.exec("SELECT MIN(at_ms) AS at_ms FROM _eda_alarm_wakeups").one().at_ms;
167
+ if (earliest === null) {
168
+ await this.storage.deleteAlarm();
169
+ return;
170
+ }
171
+ if (await this.storage.getAlarm() !== earliest) await this.storage.setAlarm(earliest);
172
+ });
173
+ return this.#tail;
174
+ }
175
+ #rows() {
176
+ return this.storage.sql.exec("SELECT owner, at_ms FROM _eda_alarm_wakeups").toArray();
177
+ }
178
+ #wakeup(owner) {
179
+ return this.storage.sql.exec("SELECT at_ms FROM _eda_alarm_wakeups WHERE owner = ?", owner).toArray()[0]?.at_ms;
180
+ }
181
+ /** Upsert or delete `owner`'s wakeup. Without `failures`, an existing count is kept. */
182
+ #write(owner, atMs, failures) {
183
+ if (atMs === void 0) {
184
+ this.storage.sql.exec("DELETE FROM _eda_alarm_wakeups WHERE owner = ?", owner);
185
+ return;
186
+ }
187
+ if (failures === void 0) {
188
+ this.storage.sql.exec(`INSERT INTO _eda_alarm_wakeups (owner, at_ms) VALUES (?, ?)
189
+ ON CONFLICT(owner) DO UPDATE SET at_ms = excluded.at_ms`, owner, atMs);
190
+ return;
191
+ }
192
+ this.storage.sql.exec(`INSERT INTO _eda_alarm_wakeups (owner, at_ms, failures) VALUES (?, ?, ?)
193
+ ON CONFLICT(owner) DO UPDATE SET at_ms = excluded.at_ms, failures = excluded.failures`, owner, atMs, failures);
194
+ }
195
+ };
196
+ const isOwner = (owner) => owner === "runtime" || owner === "transcript";
197
+ //#endregion
198
+ //#region src/durable-object.ts
199
+ /** Internal Worker-to-object header selecting an app-owned WebSocket projection. */
200
+ const EDA_WEB_SOCKET_PROJECTION_HEADER = "x-eda-websocket-projection";
201
+ /**
202
+ * Upgrade query parameter selecting the socket's stream: `transcript` selects the ordered
203
+ * transcript-window stream; anything else keeps the raw event stream.
204
+ */
205
+ const EDA_WEB_SOCKET_STREAM_PARAM = "stream";
206
+ /** Slices a backfill `transcriptStatus` call may run before it returns. */
207
+ const transcriptStatusMaxSlices = 16;
208
+ /** Server bounds on one `readTranscriptWindow` RPC, whatever limits the caller asks for. */
209
+ const transcriptWindowMaxRecords = 500;
210
+ const transcriptWindowMaxBytes = 8388608;
211
+ /** First retry delay after a failed builder alarm; doubled per consecutive failure. */
212
+ const transcriptBuilderRetryDelayMs = 3e4;
213
+ /** Slices one builder alarm runs before rescheduling itself. */
214
+ const transcriptAlarmMaxSlices = 4;
215
+ /** Resolve a concrete EDA session Durable Object binding by domain session id. */
216
+ const getEDASessionDurableObjectByName = (namespace, sessionId) => namespace.getByName(sessionId);
217
+ /**
218
+ * Base class for raw Cloudflare Durable Object hosts of one EDA session.
219
+ *
220
+ * Concrete products should extend this class, pass provider/tool layers from
221
+ * their own constructor, and register only that subclass in Wrangler. The base
222
+ * class is deliberately not exported from `workers/app.ts` and owns no binding
223
+ * name or routing convention.
224
+ */
225
+ var EDASessionDurableObject = class extends DurableObject {
226
+ #controller;
227
+ #webSocketProjectionId;
228
+ #alarms;
229
+ #transcriptOptions;
230
+ #transcript;
231
+ #transcriptEnabled;
232
+ constructor(ctx, env, options) {
233
+ super(ctx, env);
234
+ this.#webSocketProjectionId = options.webSocketProjection?.id;
235
+ const storage = toEDASessionStorage(this.ctx.storage);
236
+ this.#alarms = new DurableObjectAlarmScheduler(storage);
237
+ const { transcript: transcriptHostOptions, transcriptSockets, ...controllerOptions } = options;
238
+ const transcript = this.transcriptOptions(transcriptHostOptions);
239
+ this.#transcriptOptions = transcript;
240
+ const enabled = transcriptHostOptions?.enabled ?? false;
241
+ this.#transcriptEnabled = typeof enabled === "function" ? enabled : () => enabled;
242
+ this.#controller = new EDASessionController({
243
+ ...controllerOptions,
244
+ alarmStorage: this.#alarms.owner("runtime"),
245
+ background: this.ctx,
246
+ getWebSockets: () => this.ctx.getWebSockets(),
247
+ storage,
248
+ transcript: ({ sessionId }) => this.transcriptFor(sessionId).enabled ? transcript : void 0,
249
+ ...transcriptSockets === void 0 ? {} : { transcriptSockets: {
250
+ ...transcriptSockets,
251
+ transcriptStore: transcriptSockets.transcriptStore ?? this.transcriptPort()
252
+ } }
253
+ });
254
+ this.ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(EDA_WEB_SOCKET_PING_MESSAGE, EDA_WEB_SOCKET_PONG_MESSAGE));
255
+ this.ctx.blockConcurrencyWhile(async () => {
256
+ await Effect.runPromise(EDASessionController.migrate(storage));
257
+ await this.#alarms.adoptUnrecordedAlarm();
258
+ await this.maintainedTranscript()?.builder.start();
259
+ });
260
+ }
261
+ async fetch(request) {
262
+ if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") return new Response("Expected WebSocket upgrade.", { status: 426 });
263
+ const url = new URL(request.url);
264
+ const sessionIdRaw = url.searchParams.get("sessionId");
265
+ if (sessionIdRaw === null) return new Response("Missing sessionId.", { status: 400 });
266
+ const afterSeqRaw = url.searchParams.get("afterSeq");
267
+ const afterSeq = afterSeqRaw === null ? void 0 : Number(afterSeqRaw);
268
+ if (afterSeq !== void 0 && (!Number.isInteger(afterSeq) || afterSeq < 0)) return new Response("Invalid afterSeq.", { status: 400 });
269
+ const projectionId = request.headers.get("x-eda-websocket-projection") ?? void 0;
270
+ if (projectionId !== void 0 && projectionId !== this.#webSocketProjectionId) return new Response("Unsupported WebSocket projection.", { status: 400 });
271
+ const sessionId = this.parseSessionIdOrUndefined(sessionIdRaw);
272
+ if (sessionId === void 0) return new Response("This object does not serve that session.", { status: 400 });
273
+ const trace = traceMetadataFromRequest(request);
274
+ if (url.searchParams.get("stream") === "transcript") {
275
+ if (projectionId !== void 0 || !this.#controller.supportsTranscriptWebSocket) return new Response("Unsupported transcript stream.", { status: 400 });
276
+ if (!this.transcriptFor(sessionId).enabled) return new Response("Transcript is not enabled for this session.", { status: 409 });
277
+ const pair = new WebSocketPair();
278
+ this.ctx.acceptWebSocket(pair[1]);
279
+ this.#controller.acceptTranscriptWebSocket({
280
+ sessionId,
281
+ trace,
282
+ webSocket: pair[1]
283
+ });
284
+ return new Response(null, {
285
+ status: 101,
286
+ webSocket: pair[0]
287
+ });
288
+ }
289
+ const prepared = await this.#controller.prepareEventWebSocket({
290
+ ...afterSeq === void 0 ? {} : { afterSeq: SequenceNumber.make(afterSeq) },
291
+ sessionId,
292
+ trace,
293
+ ...projectionId === void 0 ? {} : { projectionId }
294
+ });
295
+ const pair = new WebSocketPair();
296
+ const client = pair[0];
297
+ const server = pair[1];
298
+ this.ctx.acceptWebSocket(server);
299
+ await this.#controller.acceptPreparedEventWebSocket({
300
+ ...prepared,
301
+ webSocket: server
302
+ });
303
+ return new Response(null, {
304
+ status: 101,
305
+ webSocket: client
306
+ });
307
+ }
308
+ async submit(raw) {
309
+ const input = Schema.decodeUnknownSync(SessionCommandInput)(raw);
310
+ const committed = await this.#controller.submit({
311
+ command: input.command,
312
+ sessionId: this.parseSessionId(input.sessionId),
313
+ trace: decodeTraceMetadata(input.trace)
314
+ });
315
+ return encodeEdaRpcCommittedDurableEvent(committed);
316
+ }
317
+ /** Reconcile lost grant replies from durable request and run facts. */
318
+ async runRequestOutcome(raw) {
319
+ const input = Schema.decodeUnknownSync(SessionOutcomeInput)(raw);
320
+ const result = await this.#controller.runRequestOutcome({
321
+ requestId: Schema.decodeUnknownSync(RunRequestId)(input.requestId),
322
+ sessionId: this.parseSessionId(input.sessionId),
323
+ trace: decodeTraceMetadata(input.trace)
324
+ });
325
+ return Schema.encodeSync(edaSessionRpc.runRequestOutcome.output)(result);
326
+ }
327
+ /** Trusted RPC only: the calling Worker owns scheduler authentication and authorization. */
328
+ async grantRun(raw) {
329
+ const input = Schema.decodeUnknownSync(SessionGrantRunInput)(raw);
330
+ const command = input.command;
331
+ const result = await this.#controller.grantRun({
332
+ command,
333
+ sessionId: this.parseSessionId(input.sessionId),
334
+ trace: decodeTraceMetadata(input.trace)
335
+ });
336
+ return Schema.encodeSync(edaSessionRpc.grantRun.output)(result);
337
+ }
338
+ async submitBatch(raw) {
339
+ const input = Schema.decodeUnknownSync(SessionBatchInput)(raw);
340
+ return (await this.#controller.submitBatch({
341
+ items: input.items,
342
+ sessionId: this.parseSessionId(input.sessionId),
343
+ trace: decodeTraceMetadata(input.trace)
344
+ })).map(encodeEdaRpcCommittedDurableEvent);
345
+ }
346
+ async submitAndBlock(raw) {
347
+ const input = Schema.decodeUnknownSync(SessionCommandInput)(raw);
348
+ const committed = await this.#controller.submitAndBlock({
349
+ command: input.command,
350
+ sessionId: this.parseSessionId(input.sessionId),
351
+ trace: decodeTraceMetadata(input.trace)
352
+ });
353
+ return Schema.encodeSync(edaSessionRpc.submitAndBlock.output)(committed);
354
+ }
355
+ async blockOnCommand(raw) {
356
+ const input = Schema.decodeUnknownSync(SessionBlockInput)(raw);
357
+ const committed = await this.#controller.blockOnCommand({
358
+ ...input.afterSeq === void 0 ? {} : { afterSeq: SequenceNumber.make(input.afterSeq) },
359
+ commandId: CommandId.make(input.commandId),
360
+ sessionId: this.parseSessionId(input.sessionId),
361
+ trace: decodeTraceMetadata(input.trace)
362
+ });
363
+ return Schema.encodeSync(edaSessionRpc.blockOnCommand.output)(committed);
364
+ }
365
+ async snapshot(raw) {
366
+ const input = Schema.decodeUnknownSync(SessionScopedInput)(raw);
367
+ const snapshot = await this.#controller.snapshot({
368
+ sessionId: this.parseSessionId(input.sessionId),
369
+ trace: decodeTraceMetadata(input.trace)
370
+ });
371
+ return Schema.encodeSync(edaSessionRpc.snapshot.output)(snapshot);
372
+ }
373
+ async messages(raw) {
374
+ const input = Schema.decodeUnknownSync(SessionScopedInput)(raw);
375
+ const result = await this.#controller.messages({
376
+ sessionId: this.parseSessionId(input.sessionId),
377
+ trace: decodeTraceMetadata(input.trace)
378
+ });
379
+ return Schema.encodeSync(edaSessionRpc.messages.output)(result);
380
+ }
381
+ async webSocketMessage(webSocket, message) {
382
+ await this.#controller.webSocketMessage(webSocket, message);
383
+ }
384
+ async webSocketClose(webSocket, _code, _reason, _wasClean) {
385
+ this.#controller.webSocketClose(webSocket);
386
+ }
387
+ async webSocketError(webSocket, _error) {
388
+ this.#controller.webSocketError(webSocket);
389
+ }
390
+ async destroySession(raw) {
391
+ const input = Schema.decodeUnknownSync(SessionScopedInput)(raw);
392
+ await this.#controller.destroy({
393
+ sessionId: this.parseSessionId(input.sessionId),
394
+ trace: decodeTraceMetadata(input.trace)
395
+ });
396
+ await this.ctx.storage.deleteAlarm();
397
+ await this.ctx.storage.deleteAll();
398
+ Effect.runSync(EDASessionController.migrate(toEDASessionStorage(this.ctx.storage)));
399
+ }
400
+ /**
401
+ * Storage-only backfill entry point: advance pending transcript build work by bounded slices
402
+ * and return the transcript status. It never starts the model runtime, reducers or sinks.
403
+ */
404
+ async transcriptStatus(raw) {
405
+ const input = Schema.decodeUnknownSync(SessionScopedInput)(raw);
406
+ const transcript = this.transcriptFor(this.parseSessionId(input.sessionId));
407
+ if (!transcript.enabled) return Schema.encodeSync(edaSessionRpc.transcriptStatus.output)(transcript.store.disabledStatus());
408
+ await transcript.builder.run(transcriptStatusMaxSlices);
409
+ return Schema.encodeSync(edaSessionRpc.transcriptStatus.output)(transcript.store.status());
410
+ }
411
+ /** Runtime-free transcript window read for SSR and CLIs. Hosts authorize the caller. */
412
+ async readTranscriptWindow(raw) {
413
+ const input = Schema.decodeUnknownSync(SessionTranscriptWindowInput)(raw);
414
+ const transcript = this.transcriptFor(this.parseSessionId(input.sessionId));
415
+ if (!transcript.enabled) return Schema.encodeSync(edaSessionRpc.readTranscriptWindow.output)({ _tag: "Disabled" });
416
+ const limits = input.limits ?? defaultTranscriptWindowLimits;
417
+ const window = transcript.store.readWindow(input.selector, {
418
+ ...limits,
419
+ maxRecords: Math.min(limits.maxRecords, transcriptWindowMaxRecords),
420
+ maxBytes: Math.min(limits.maxBytes, transcriptWindowMaxBytes)
421
+ });
422
+ return Schema.encodeSync(edaSessionRpc.readTranscriptWindow.output)(Result.isSuccess(window) ? {
423
+ _tag: "Window",
424
+ window: window.success
425
+ } : {
426
+ _tag: "CursorMismatch",
427
+ message: window.failure.message
428
+ });
429
+ }
430
+ /**
431
+ * Runtime-free read of the body a transcript `Ref` names, resolved through the immutable log
432
+ * (so it stays valid after projection rows are rebuilt). Hosts authorize the caller.
433
+ */
434
+ async readTranscriptBody(raw) {
435
+ const input = Schema.decodeUnknownSync(SessionTranscriptBodyInput)(raw);
436
+ const scope = this.transcriptReadScope(input.sessionId, input.incarnation);
437
+ if (scope._tag !== "Scope") return Schema.encodeSync(edaSessionRpc.readTranscriptBody.output)(scope);
438
+ const sessionId = scope.sessionId;
439
+ const committed = readDurableObjectCommittedEvent(toEDASessionStorage(this.ctx.storage).sql, sessionId, input.source.seq);
440
+ const resolved = committed === void 0 ? {
441
+ _tag: "Missing",
442
+ message: `no event at seq ${input.source.seq}`
443
+ } : resolveTranscriptBody(committed.event, input.source.path);
444
+ return Schema.encodeSync(edaSessionRpc.readTranscriptBody.output)(resolved._tag === "Found" ? {
445
+ _tag: "Body",
446
+ value: resolved.value
447
+ } : {
448
+ _tag: "NotFound",
449
+ message: resolved.message
450
+ });
451
+ }
452
+ /** Runtime-free read of a compaction summary's text. Hosts authorize the caller. */
453
+ async readTranscriptSummary(raw) {
454
+ const input = Schema.decodeUnknownSync(SessionTranscriptSummaryInput)(raw);
455
+ const scope = this.transcriptReadScope(input.sessionId, input.incarnation);
456
+ if (scope._tag !== "Scope") return Schema.encodeSync(edaSessionRpc.readTranscriptSummary.output)(scope);
457
+ const artifact = readDurableObjectSummary(toEDASessionStorage(this.ctx.storage).sql, input.summaryId);
458
+ return Schema.encodeSync(edaSessionRpc.readTranscriptSummary.output)(artifact === void 0 ? {
459
+ _tag: "NotFound",
460
+ message: `no summary ${input.summaryId}`
461
+ } : {
462
+ _tag: "Summary",
463
+ summary: {
464
+ compactionId: artifact.compactionId,
465
+ summaryId: artifact.summaryId,
466
+ text: artifact.text,
467
+ sourceFromSeq: artifact.sourceFromSeq,
468
+ sourceToSeq: artifact.sourceToSeq
469
+ }
470
+ });
471
+ }
472
+ /**
473
+ * The alarm is shared by the runtime heartbeat and the transcript builder; see
474
+ * `runEDASessionAlarm`.
475
+ */
476
+ async alarm() {
477
+ const transcript = this.maintainedTranscript();
478
+ await runEDASessionAlarm({
479
+ alarms: this.#alarms,
480
+ now: () => Date.now(),
481
+ runRuntime: async () => {
482
+ const sessionId = this.sessionIdFromObjectName();
483
+ await this.#controller.alarm(sessionId === void 0 ? void 0 : {
484
+ sessionId,
485
+ trace: makeRootEDATraceMetadata()
486
+ });
487
+ },
488
+ runBuilderSlices: transcript === void 0 ? void 0 : () => transcript.builder.runSlices(transcriptAlarmMaxSlices),
489
+ maxBuilderFailures: this.#transcriptOptions.sliceLimits?.maxSliceFailures ?? defaultTranscriptSliceLimits.maxSliceFailures,
490
+ builderRetryDelayMs: transcriptBuilderRetryDelayMs,
491
+ onBuilderFailure: ({ error, consecutiveFailures, stopped }) => {
492
+ console.error("EDA transcript builder slice failed", error);
493
+ if (stopped) transcript?.store.alert({
494
+ _tag: "BuilderStopped",
495
+ consecutiveFailures,
496
+ error: error instanceof Error ? error.message : String(error)
497
+ });
498
+ }
499
+ });
500
+ }
501
+ transcriptOptions(options) {
502
+ const key = options?.adoptIncarnationKey;
503
+ return {
504
+ definitions: options?.definitions ?? { current: Effect.runSync(makeTranscriptDefinition(emptyTranscriptMappings)) },
505
+ ...key === void 0 ? {} : { adoptIncarnation: {
506
+ key,
507
+ kv: this.ctx.storage.kv
508
+ } },
509
+ sliceLimits: resolveTranscriptSliceLimits(options?.sliceLimits),
510
+ observer: {
511
+ onAlert: options?.onAlert ?? ((alert) => console.error("EDA transcript alert", JSON.stringify(alert))),
512
+ onPendingWork: () => {
513
+ const builder = this.maintainedTranscript()?.builder;
514
+ if (builder !== void 0) this.ctx.waitUntil(builder.kick());
515
+ },
516
+ onServedChanged: () => {
517
+ this.ctx.waitUntil(this.#controller.refreshTranscriptWebSockets());
518
+ },
519
+ onActivated: (activation) => {
520
+ this.ctx.waitUntil(this.#controller.refreshTranscriptWebSockets());
521
+ options?.onActivated?.(activation);
522
+ }
523
+ }
524
+ };
525
+ }
526
+ /** This object's transcript store as the port transcript sockets read, resolved per call. */
527
+ transcriptPort() {
528
+ const port = () => this.maintainedTranscript()?.store.port;
529
+ const unbound = () => Effect.fail(new EDASessionStoreError({ message: this.objectTranscript()?.enabled === false ? "EDA transcript is not enabled for this session" : "EDA transcript read before the session was named" }));
530
+ return {
531
+ readTranscriptWindow: (selector, limits) => Effect.suspend(() => port()?.readTranscriptWindow(selector, limits) ?? unbound()),
532
+ readTranscriptMeta: Effect.suspend(() => port()?.readTranscriptMeta ?? unbound())
533
+ };
534
+ }
535
+ /**
536
+ * The session and log a body or summary read may serve: pinned to the session this object's
537
+ * transcript serves, like window reads, and to the incarnation the caller's reference names.
538
+ */
539
+ transcriptReadScope(rawSessionId, incarnation) {
540
+ const transcript = this.transcriptFor(this.parseSessionId(rawSessionId));
541
+ if (!transcript.enabled) return { _tag: "Disabled" };
542
+ const current = transcript.store.incarnation();
543
+ return incarnation === current ? {
544
+ _tag: "Scope",
545
+ sessionId: transcript.sessionId
546
+ } : {
547
+ _tag: "IncarnationMismatch",
548
+ incarnation: current
549
+ };
550
+ }
551
+ /** The transcript service of this object's session, created on first use. */
552
+ transcriptFor(sessionId) {
553
+ if (this.#transcript !== void 0) {
554
+ if (this.#transcript.sessionId !== sessionId) throw new Error(`EDA transcript is scoped to session ${this.#transcript.sessionId}; received ${sessionId}`);
555
+ return this.#transcript;
556
+ }
557
+ const store = new DurableObjectTranscriptStore({
558
+ sessionId,
559
+ storage: toEDASessionStorage(this.ctx.storage),
560
+ transcript: this.#transcriptOptions
561
+ });
562
+ const builder = new DurableObjectTranscriptBuilder(store, () => this.#alarms.request("transcript", Date.now()));
563
+ this.#transcript = {
564
+ sessionId,
565
+ enabled: this.#transcriptEnabled(sessionId),
566
+ store,
567
+ builder
568
+ };
569
+ return this.#transcript;
570
+ }
571
+ /** The transcript service when this object's session is enabled for maintenance. */
572
+ maintainedTranscript() {
573
+ const transcript = this.objectTranscript();
574
+ return transcript?.enabled === true ? transcript : void 0;
575
+ }
576
+ /** The transcript service when the object is named by a valid session id. */
577
+ objectTranscript() {
578
+ if (this.#transcript !== void 0) return this.#transcript;
579
+ const name = this.ctx.id.name;
580
+ return name !== void 0 && Schema.is(SessionId)(name) ? this.transcriptFor(SessionId.make(name)) : void 0;
581
+ }
582
+ /** `parseSessionId` for request paths that answer a foreign session with a 400. */
583
+ parseSessionIdOrUndefined(input) {
584
+ if (!Schema.is(SessionId)(input)) return void 0;
585
+ const name = this.ctx.id.name;
586
+ if (name !== void 0 && name !== input) return void 0;
587
+ const bound = this.#transcript?.sessionId;
588
+ return bound === void 0 || bound === input ? SessionId.make(input) : void 0;
589
+ }
590
+ parseSessionId(input) {
591
+ const sessionId = SessionId.make(input);
592
+ const objectName = this.ctx.id.name;
593
+ if (objectName !== void 0 && objectName !== sessionId) throw new Error(`EDA Durable Object name ${objectName} cannot serve session ${sessionId}`);
594
+ return sessionId;
595
+ }
596
+ sessionIdFromObjectName() {
597
+ return this.ctx.id.name === void 0 ? void 0 : SessionId.make(this.ctx.id.name);
598
+ }
599
+ };
600
+ const toEDASessionStorage = (storage) => {
601
+ return storage;
602
+ };
603
+ const decodeTraceMetadata = (input) => Schema.decodeUnknownSync(EDATraceMetadata)(input);
604
+ const traceMetadataFromRequest = (request) => {
605
+ const parent = parseEDATraceparent(request.headers.get("traceparent"), request.headers.get("tracestate"));
606
+ return parent === null ? makeRootEDATraceMetadata() : makeEDATraceMetadataFromParent(parent);
607
+ };
608
+ /** Convenience config constructor for subclasses that need only provider/model ids. */
609
+ const edaRuntimeConfig = (input) => ({
610
+ modelSelection: {
611
+ modelId: input.modelId,
612
+ provider: input.provider
613
+ },
614
+ ...input.maxToolCallsPerRun === void 0 ? {} : { maxToolCallsPerRun: input.maxToolCallsPerRun },
615
+ ...input.systemPrompt === void 0 ? {} : { systemPrompt: input.systemPrompt }
616
+ });
617
+ /** Encode committed events into the existing structured-clone envelope. */
618
+ const encodeEdaRpcCommittedDurableEvent = (entry) => ({
619
+ position: entry.position,
620
+ event: encodeEdaRpcDurableEvent(durableEventEnvelope(entry.event))
621
+ });
622
+ /** Encode query snapshots without asserting that encoded command values are decoded classes. */
623
+ const encodeEdaRpcSessionSnapshot = Schema.encodeSync(edaSessionRpc.snapshot.output);
624
+ //#endregion
625
+ export { encodeEdaRpcCommittedDurableEvent as a, DurableObjectAlarmScheduler as c, edaRuntimeConfig as i, EDA_WEB_SOCKET_PROJECTION_HEADER as n, encodeEdaRpcSessionSnapshot as o, EDA_WEB_SOCKET_STREAM_PARAM as r, getEDASessionDurableObjectByName as s, EDASessionDurableObject as t };
626
+
627
+ //# sourceMappingURL=durable-object-CkrxIOH4.js.map