effect-durable-agent-cloudflare 0.1.0-alpha.4 → 0.1.0-alpha.7

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.
@@ -1,38 +1,25 @@
1
- import { t as websocket_protocol_exports } from "./websocket-protocol.js";
2
1
  import * as Effect from "effect/Effect";
3
2
  import * as Schema from "effect/Schema";
4
3
  import { DurableEventEnvelope, assistantMessageCommittedEventType, assistantMessageImportedEventType, assistantPartialCommittedEventType, commandAdmittedEventType, commandCancelledEventType, commandCompletedEventType, commandFailedEventType, commandStartedEventType, contextRebasedEventType, decodeUnknownEDADurableEventSync, effectDurableAgentNamespace, steeringMessageQueuedEventType, summaryCreatedEventType, systemMessageCommittedEventType, userMessageCommittedEventType } from "effect-durable-agent/types/events";
5
- import { EDACommand } from "effect-durable-agent/types/commands";
6
- import { SequenceNumber, SummaryId, durablePosition } from "effect-durable-agent/types/core";
7
- import { makeRootEDATraceMetadata } from "effect-durable-agent/types/tracing";
8
- import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
9
- import * as Deferred from "effect/Deferred";
10
- import * as Exit from "effect/Exit";
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";
11
7
  import * as Layer from "effect/Layer";
12
8
  import * as ManagedRuntime from "effect/ManagedRuntime";
13
- import * as Queue from "effect/Queue";
14
- import * as Redacted from "effect/Redacted";
15
- import "effect/Tracer";
16
- import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
17
- import "effect/unstable/http/HttpBody";
18
- import * as HttpClient from "effect/unstable/http/HttpClient";
19
- import * as HttpClientError from "effect/unstable/http/HttpClientError";
20
- import "effect/unstable/http/HttpClientRequest";
21
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
22
- import "effect-durable-agent/services/compaction";
23
- import { EDAKeepAlive } from "effect-durable-agent/services/keep-alive";
24
- import "effect-durable-agent/services/prompt-projector";
25
- import { EDAReducerRegistry } from "effect-durable-agent/services/reducer-registry";
26
9
  import { EDARuntime } from "effect-durable-agent/services/runtime";
27
- import { makeEDARuntimeLayer } from "effect-durable-agent/services/runtime-layer";
28
10
  import { CommittedDurableEvent, EDASessionStore, EDASessionStoreError } from "effect-durable-agent/services/session-store";
29
- import { annotateEdaSpan, committedBatchAttributes, compactSpanAttributes, toEdaExternalSpan } from "effect-durable-agent/services/tracing";
30
- import { SubscriberLagged, SubscriberProtocolError, SubscriberSendFailed, runWebSocketSubscriber } from "effect-durable-agent/services/websocket-subscriber";
11
+ import { SinkCheckpointStore } from "effect-durable-agent/services/sink-checkpoint-store";
31
12
  import * as Cause from "effect/Cause";
32
13
  import * as Option from "effect/Option";
33
14
  import * as Stream from "effect/Stream";
34
15
  import { CompactionSummaryArtifact } from "effect-durable-agent/domain/context-projection";
35
- import { SinkCheckpointStore } from "effect-durable-agent/services/sink-checkpoint-store";
16
+ import "effect-durable-agent/services/compaction";
17
+ import { EDAKeepAlive } from "effect-durable-agent/services/keep-alive";
18
+ import "effect-durable-agent/services/prompt-projector";
19
+ import { EDAReducerRegistry } from "effect-durable-agent/services/reducer-registry";
20
+ import { makeEDARuntimeLayer } from "effect-durable-agent/services/runtime-layer";
21
+ import { SessionEventObserver } from "effect-durable-agent/services/session-event-observer";
22
+ 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";
36
23
  /**
37
24
  * Alarm-backed in-memory keepalive for a raw Durable Object host.
38
25
  *
@@ -155,6 +142,78 @@ var DurableObjectKeepAlive = class {
155
142
  }
156
143
  };
157
144
  //#endregion
145
+ //#region src/durable-object-sink-checkpoints.ts
146
+ /**
147
+ * Durable Object SQLite adapter for EDA sink checkpoints.
148
+ *
149
+ * Checkpoint SQL stays in the host layer for the same reason session storage SQL
150
+ * does: framework sink code should depend on checkpoint semantics, not table access.
151
+ */
152
+ var DurableObjectSinkCheckpointStore = class DurableObjectSinkCheckpointStore {
153
+ /** Ensure the fixed `_eda_sink_cursors` table exists during host startup. */
154
+ static migrate = (storage) => migrateSinkCheckpointStore(storage);
155
+ /** Build the semantic checkpoint store over native DO SQLite. */
156
+ static make = (storage) => makeDurableObjectSinkCheckpointStore(storage);
157
+ static layer = (storage) => Layer.effect(SinkCheckpointStore, DurableObjectSinkCheckpointStore.make(storage));
158
+ };
159
+ /** Create the sink checkpoint table inside the current EDA storage schema. */
160
+ const migrateSinkCheckpointStore = (storage) => Effect.try({
161
+ try: () => {
162
+ storage.sql.exec(`
163
+ CREATE TABLE IF NOT EXISTS _eda_sink_cursors (
164
+ sink_name TEXT PRIMARY KEY,
165
+ after_seq INTEGER NOT NULL,
166
+ payload_json TEXT NOT NULL
167
+ )
168
+ `);
169
+ },
170
+ catch: (error) => sinkCheckpointError("migrating SinkCheckpointStore", error)
171
+ });
172
+ /** Interpret checkpoint load/save/commit with idempotent upserts. */
173
+ const makeDurableObjectSinkCheckpointStore = (storage) => Effect.gen(function* () {
174
+ yield* DurableObjectSinkCheckpointStore.migrate(storage);
175
+ const { sql } = storage;
176
+ return {
177
+ load: (sinkName) => Effect.try({
178
+ try: () => {
179
+ const row = sql.exec(`SELECT after_seq, payload_json FROM _eda_sink_cursors WHERE sink_name = ?`, sinkName).toArray()[0];
180
+ return row === void 0 ? {
181
+ afterSeq: SequenceNumber.make(0),
182
+ payload: void 0
183
+ } : {
184
+ afterSeq: SequenceNumber.make(row.after_seq),
185
+ payload: JSON.parse(row.payload_json)
186
+ };
187
+ },
188
+ catch: (error) => sinkCheckpointError("reading sink checkpoint", error)
189
+ }),
190
+ saveState: (sinkName, payload) => Effect.try({
191
+ try: () => {
192
+ sql.exec(`
193
+ INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
194
+ VALUES (?, 0, ?)
195
+ ON CONFLICT(sink_name) DO UPDATE SET
196
+ payload_json = excluded.payload_json
197
+ `, sinkName, JSON.stringify(payload));
198
+ },
199
+ catch: (error) => sinkCheckpointError("saving sink checkpoint state", error)
200
+ }),
201
+ commit: (sinkName, afterSeq, payload) => Effect.try({
202
+ try: () => {
203
+ sql.exec(`
204
+ INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
205
+ VALUES (?, ?, ?)
206
+ ON CONFLICT(sink_name) DO UPDATE SET
207
+ after_seq = excluded.after_seq,
208
+ payload_json = excluded.payload_json
209
+ `, sinkName, afterSeq, JSON.stringify(payload ?? { updatedAtMs: Date.now() }));
210
+ },
211
+ catch: (error) => sinkCheckpointError("committing sink checkpoint", error)
212
+ })
213
+ };
214
+ });
215
+ const sinkCheckpointError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
216
+ //#endregion
158
217
  //#region src/durable-object-store.ts
159
218
  /** Safe payload ceiling below the Durable Object SQLite 2 MB value limit. */
160
219
  /** Conservative JSON limit below the Durable Object SQLite per-value ceiling. */
@@ -776,422 +835,87 @@ const stringifyJsonForColumn = (value, column) => {
776
835
  };
777
836
  const durableStoreError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
778
837
  //#endregion
779
- //#region src/durable-object-sink-checkpoints.ts
780
- /**
781
- * Durable Object SQLite adapter for EDA sink checkpoints.
782
- *
783
- * Checkpoint SQL stays in the host layer for the same reason session storage SQL
784
- * does: framework sink code should depend on checkpoint semantics, not table access.
785
- */
786
- var DurableObjectSinkCheckpointStore = class DurableObjectSinkCheckpointStore {
787
- /** Ensure the fixed `_eda_sink_cursors` table exists during host startup. */
788
- static migrate = (storage) => migrateSinkCheckpointStore(storage);
789
- /** Build the semantic checkpoint store over native DO SQLite. */
790
- static make = (storage) => makeDurableObjectSinkCheckpointStore(storage);
791
- static layer = (storage) => Layer.effect(SinkCheckpointStore, DurableObjectSinkCheckpointStore.make(storage));
792
- };
793
- /** Create the sink checkpoint table inside the current EDA storage schema. */
794
- const migrateSinkCheckpointStore = (storage) => Effect.try({
795
- try: () => {
796
- storage.sql.exec(`
797
- CREATE TABLE IF NOT EXISTS _eda_sink_cursors (
798
- sink_name TEXT PRIMARY KEY,
799
- after_seq INTEGER NOT NULL,
800
- payload_json TEXT NOT NULL
801
- )
802
- `);
803
- },
804
- catch: (error) => sinkCheckpointError("migrating SinkCheckpointStore", error)
805
- });
806
- /** Interpret checkpoint load/save/commit with idempotent upserts. */
807
- const makeDurableObjectSinkCheckpointStore = (storage) => Effect.gen(function* () {
808
- yield* DurableObjectSinkCheckpointStore.migrate(storage);
809
- const { sql } = storage;
810
- return {
811
- load: (sinkName) => Effect.try({
812
- try: () => {
813
- const row = sql.exec(`SELECT after_seq, payload_json FROM _eda_sink_cursors WHERE sink_name = ?`, sinkName).toArray()[0];
814
- return row === void 0 ? {
815
- afterSeq: SequenceNumber.make(0),
816
- payload: void 0
817
- } : {
818
- afterSeq: SequenceNumber.make(row.after_seq),
819
- payload: JSON.parse(row.payload_json)
820
- };
821
- },
822
- catch: (error) => sinkCheckpointError("reading sink checkpoint", error)
823
- }),
824
- saveState: (sinkName, payload) => Effect.try({
825
- try: () => {
826
- sql.exec(`
827
- INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
828
- VALUES (?, 0, ?)
829
- ON CONFLICT(sink_name) DO UPDATE SET
830
- payload_json = excluded.payload_json
831
- `, sinkName, JSON.stringify(payload));
832
- },
833
- catch: (error) => sinkCheckpointError("saving sink checkpoint state", error)
834
- }),
835
- commit: (sinkName, afterSeq, payload) => Effect.try({
836
- try: () => {
837
- sql.exec(`
838
- INSERT INTO _eda_sink_cursors (sink_name, after_seq, payload_json)
839
- VALUES (?, ?, ?)
840
- ON CONFLICT(sink_name) DO UPDATE SET
841
- after_seq = excluded.after_seq,
842
- payload_json = excluded.payload_json
843
- `, sinkName, afterSeq, JSON.stringify(payload ?? { updatedAtMs: Date.now() }));
844
- },
845
- catch: (error) => sinkCheckpointError("committing sink checkpoint", error)
846
- })
847
- };
848
- });
849
- const sinkCheckpointError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
850
- //#endregion
851
- //#region src/durable-object-runtime.ts
852
- const resolveRuntimeFactory = (value, sessionId) => typeof value === "function" ? value({ sessionId }) : value;
853
- /**
854
- * Compose the full EDA service graph for one raw Durable Object session.
855
- *
856
- * The layer is intentionally host-owned: tests can pass a fake model layer,
857
- * while the Worker host can pass a provider-backed layer from Env.
858
- */
859
- const makeEDADurableObjectRuntimeLayer = ({ config, compactionExecutorLayer, compactionPolicyLayer, keepAlive, modelLayer, promptProjectorLayer, reducers, sessionId, sinks, storage, toolkit, toolRegistry, tracer }) => {
860
- const Store = DurableObjectSessionStore.layer({
861
- sessionId,
862
- storage
863
- });
864
- const KeepAlive = keepAlive === void 0 ? EDAKeepAlive.Noop : EDAKeepAlive.FromAcquire(() => keepAlive.acquire());
865
- const SinkCheckpoints = DurableObjectSinkCheckpointStore.layer(storage);
838
+ //#region src/runtime/runtime-layer.ts
839
+ /** Compose the platform-neutral runtime with Cloudflare persistence and lifecycle adapters. */
840
+ const makeEDADurableObjectRuntimeLayer = ({ config, compactionExecutorLayer, compactionPolicyLayer, keepAlive, modelLayer, promptProjectorLayer, reducers, sessionEventObserverLayer, sessionId, sinks, storage, toolkit, toolRegistry, tracer }) => {
866
841
  return makeEDARuntimeLayer({
867
842
  config,
868
843
  compactionExecutorLayer,
869
844
  compactionPolicyLayer,
870
- keepAliveLayer: KeepAlive,
845
+ keepAliveLayer: keepAlive === void 0 ? EDAKeepAlive.Noop : EDAKeepAlive.FromAcquire(() => keepAlive.acquire()),
871
846
  modelLayer,
872
847
  promptProjectorLayer,
873
848
  reducerRegistryLayer: EDAReducerRegistry.Live(reducers ?? []),
849
+ sessionEventObserverLayer,
874
850
  sessionId,
875
- sessionStoreLayer: Store,
876
- sinkCheckpointStoreLayer: SinkCheckpoints,
851
+ sessionStoreLayer: DurableObjectSessionStore.layer({
852
+ sessionId,
853
+ storage
854
+ }),
855
+ sinkCheckpointStoreLayer: DurableObjectSinkCheckpointStore.layer(storage),
877
856
  sinks,
878
857
  toolkit,
879
858
  toolRegistry,
880
859
  tracer
881
860
  });
882
861
  };
883
- /** OpenAI-backed Effect AI model layer for the default Worker host. */
884
- const makeEDADurableObjectOpenAiModelLayer = (options) => {
885
- const apiKey = optionalNonEmptyString(options.apiKey);
886
- if (apiKey === void 0 && options.aiGateway === void 0) throw new Error("OPENAI_API_KEY is required unless the EDA OpenAI model layer is backed by a Cloudflare AI Gateway binding.");
887
- const baseHttpClient = options.aiGateway === void 0 ? FetchHttpClient.layer : Layer.succeed(HttpClient.HttpClient, makeCloudflareAiGatewayHttpClient(options.aiGateway));
888
- const client = OpenAiClient.layer({
889
- ...apiKey === void 0 ? {} : { apiKey: Redacted.make(apiKey) },
890
- ...options.apiUrl === void 0 ? {} : { apiUrl: options.apiUrl }
891
- }).pipe(Layer.provide(baseHttpClient));
892
- return OpenAiLanguageModel.layer({
893
- model: options.modelId,
894
- ...options.config === void 0 ? {} : { config: options.config }
895
- }).pipe(Layer.provide(client));
896
- };
897
- const OPENAI_API_BASE_PATH_PREFIX = "/v1/";
898
- const makeCloudflareAiGatewayHttpClient = (gateway) => HttpClient.make((request, url, signal) => Effect.tryPromise({
899
- try: async () => {
900
- const response = await gateway.run({
901
- endpoint: openAiGatewayEndpointFromUrl(url),
902
- headers: requestHeadersForGateway(request),
903
- provider: "openai",
904
- query: requestBodyForGateway(request.body)
905
- }, { signal });
906
- return HttpClientResponse.fromWeb(request, response);
907
- },
908
- catch: (cause) => new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({
909
- request,
910
- cause,
911
- description: "Cloudflare AI Gateway request failed"
912
- }) })
913
- }));
914
- const openAiGatewayEndpointFromUrl = (url) => {
915
- return `${url.pathname.startsWith(OPENAI_API_BASE_PATH_PREFIX) ? url.pathname.slice(4) : url.pathname.replace(/^\/+/, "")}${url.search}`;
916
- };
917
- const requestHeadersForGateway = (request) => Object.fromEntries(Object.entries(request.headers).filter((entry) => {
918
- const [key, value] = entry;
919
- return typeof value === "string" && key.toLowerCase() !== "host";
920
- }));
921
- const requestBodyForGateway = (body) => {
922
- switch (body._tag) {
923
- case "Empty": return;
924
- case "Raw": return body.body;
925
- case "Uint8Array": return JSON.parse(new TextDecoder().decode(body.body));
926
- default: throw new Error(`Unsupported OpenAI request body for Cloudflare AI Gateway: ${body._tag}`);
927
- }
928
- };
929
- const optionalNonEmptyString = (value) => {
930
- const trimmed = value?.trim();
931
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
932
- };
933
- const hostSpanOptions = (trace, attributes) => ({
934
- kind: "server",
935
- attributes: compactSpanAttributes({
936
- ...trace.attributes,
937
- ...attributes
938
- }),
939
- links: trace.links.map((link) => ({
940
- span: toEdaExternalSpan(link.context),
941
- attributes: link.attributes
942
- })),
943
- ...trace.parent === null ? {} : { parent: toEdaExternalSpan(trace.parent) }
944
- });
945
- /** Raw Durable Object host controller for one EDA session object. */
946
- var EDASessionDurableObjectHost = class {
862
+ //#endregion
863
+ //#region src/runtime/session-runtime.ts
864
+ /** Owns the disposable ManagedRuntime and nothing about WebSocket transport. */
865
+ var EDASessionRuntime = class {
947
866
  options;
948
867
  keepAlive;
949
- eventSockets = /* @__PURE__ */ new WeakMap();
950
- runtimeState;
951
- runtimePromise;
868
+ state;
869
+ building;
952
870
  constructor(options) {
953
871
  this.options = options;
954
872
  this.keepAlive = options.keepAlive ?? new DurableObjectKeepAlive(options.storage, options.background);
955
873
  }
956
- /** Run idempotent host and event-store migrations. Safe from `blockConcurrencyWhile`. */
957
874
  static migrate = (storage) => Effect.gen(function* () {
958
875
  yield* DurableObjectSessionStore.migrate(storage);
959
876
  yield* DurableObjectSinkCheckpointStore.migrate(storage);
960
877
  });
961
- /** Admit one command durably. Active runtime work holds EDA keep-alive leases internally. */
962
- async submit(input) {
963
- const runtime = await this.getRuntime(input.sessionId);
964
- const trace = input.trace ?? makeRootEDATraceMetadata();
965
- return await runtime.runtime.runPromise(Effect.gen(function* () {
966
- return yield* (yield* EDARuntime).submit(input.command);
967
- }).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
968
- "eda.session.id": input.sessionId,
969
- "eda.command.kind": input.command._tag,
970
- "eda.command.id": input.command.commandId
971
- }))));
878
+ isReady(sessionId) {
879
+ return this.state?.sessionId === sessionId;
972
880
  }
973
- /** Submit an ordered durable batch of commands and app events. */
974
- async submitBatch(input) {
975
- const runtime = await this.getRuntime(input.sessionId);
976
- const trace = input.trace ?? makeRootEDATraceMetadata();
977
- return await runtime.runtime.runPromise(Effect.gen(function* () {
978
- return yield* (yield* EDARuntime).submit(input.items);
979
- }).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
980
- "eda.session.id": input.sessionId,
981
- "eda.command.submittable_count": input.items.length,
982
- "eda.command.count": input.items.filter((item) => "_tag" in item).length
983
- }))));
984
- }
985
- /** Admit one command and wait for its terminal command boundary. */
986
- async submitAndBlock(input) {
987
- const runtime = await this.getRuntime(input.sessionId);
988
- const trace = input.trace ?? makeRootEDATraceMetadata();
989
- return await runtime.runtime.runPromise(Effect.gen(function* () {
990
- return yield* (yield* EDARuntime).submitAndBlock(input.command);
991
- }).pipe(Effect.withSpan("agent.command.submit.wait", hostSpanOptions(trace, {
992
- "eda.session.id": input.sessionId,
993
- "eda.command.kind": input.command._tag,
994
- "eda.command.id": input.command.commandId
995
- }))));
881
+ async prepare(sessionId) {
882
+ await this.get(sessionId);
996
883
  }
997
- /** Wait for an existing command to reach its terminal command boundary. */
998
- async blockOnCommand(input) {
999
- const runtime = await this.getRuntime(input.sessionId);
1000
- const trace = input.trace ?? makeRootEDATraceMetadata();
1001
- return await runtime.runtime.runPromise(Effect.gen(function* () {
1002
- return yield* (yield* EDARuntime).blockOnCommand(input.commandId, input.afterSeq ?? SequenceNumber.make(0));
1003
- }).pipe(Effect.withSpan("agent.command.wait", hostSpanOptions(trace, {
1004
- "eda.session.id": input.sessionId,
1005
- "eda.command.id": input.commandId,
1006
- "eda.seq.after": input.afterSeq
1007
- }))));
1008
- }
1009
- /** Read the authoritative live session snapshot. */
1010
- snapshot(input) {
1011
- const trace = input.trace ?? makeRootEDATraceMetadata();
1012
- return this.withRuntime(input.sessionId, (eda) => eda.snapshot().pipe(Effect.withSpan("agent.session.snapshot", hostSpanOptions(trace, { "eda.session.id": input.sessionId }))));
1013
- }
1014
- /** Read durable transcript messages in committed order. */
1015
- messages(input) {
1016
- const trace = input.trace ?? makeRootEDATraceMetadata();
1017
- return this.withRuntime(input.sessionId, (eda) => eda.messages().pipe(Effect.withSpan("agent.messages.list", hostSpanOptions(trace, { "eda.session.id": input.sessionId }))));
1018
- }
1019
- /** Accept one WebSocket as the live event stream for this session. */
1020
- async acceptEventWebSocket(input) {
1021
- const attachment = websocket_protocol_exports.EDAWebSocketAttachment.make({
1022
- kind: "eda-events-v1",
1023
- sessionId: input.sessionId,
1024
- subscriberId: websocket_protocol_exports.SubscriberId.make(crypto.randomUUID()),
1025
- lastAckedSeq: input.afterSeq ?? SequenceNumber.make(0),
1026
- trace: input.trace ?? makeRootEDATraceMetadata()
1027
- });
1028
- input.webSocket.serializeAttachment(attachment);
1029
- await this.startEventWebSocket(input.webSocket, attachment);
1030
- }
1031
- /** Restore hibernated event WebSockets whose in-memory subscriber fibers disappeared. */
1032
- async restoreEventWebSockets(webSockets) {
1033
- for (const webSocket of webSockets) {
1034
- if (this.eventSockets.has(webSocket)) continue;
1035
- const decoded = await decodeWebSocketAttachment(webSocket.deserializeAttachment());
1036
- if (decoded._tag === "Decoded") await this.startEventWebSocket(webSocket, decoded.attachment);
1037
- else if (decoded._tag === "Malformed") closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_PROTOCOL_ERROR, "attachment");
1038
- }
884
+ async run(sessionId, operation) {
885
+ return await (await this.get(sessionId)).runtime.runPromise(Effect.gen(function* () {
886
+ return yield* operation(yield* EDARuntime);
887
+ }));
1039
888
  }
1040
- /** Route one inbound WebSocket message into the subscriber ACK queue. */
1041
- async webSocketMessage(webSocket, message) {
1042
- const state = await this.ensureEventWebSocket(webSocket);
1043
- if (state === void 0) {
1044
- closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1045
- return;
1046
- }
1047
- if (typeof message !== "string") {
1048
- closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_PROTOCOL_ERROR, "binary");
1049
- state.interrupt?.();
1050
- return;
1051
- }
1052
- try {
1053
- const frame = await Effect.runPromise((0, websocket_protocol_exports.decodeEDAWebSocketClientMessage)(message));
1054
- await Effect.runPromise(Queue.offer(state.incoming, frame));
1055
- } catch {
1056
- closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1057
- state.interrupt?.();
1058
- }
889
+ async alarm(sessionId) {
890
+ const resolved = sessionId ?? this.state?.sessionId;
891
+ if (resolved !== void 0) await this.get(resolved);
892
+ await this.keepAlive.alarm();
1059
893
  }
1060
- /** Interrupt subscriber work after the client closes the WebSocket. */
1061
- async webSocketClose(webSocket) {
1062
- const state = this.eventSockets.get(webSocket);
1063
- if (state === void 0) return;
1064
- await Effect.runPromise(Deferred.succeed(state.closed, void 0).pipe(Effect.ignore));
1065
- state.interrupt?.();
1066
- this.eventSockets.delete(webSocket);
1067
- }
1068
- /** Interrupt subscriber work after a host WebSocket error. */
1069
- async webSocketError(webSocket) {
1070
- const state = this.eventSockets.get(webSocket);
1071
- if (state === void 0) return;
1072
- await Effect.runPromise(Deferred.succeed(state.closed, void 0).pipe(Effect.ignore));
1073
- state.interrupt?.();
1074
- this.eventSockets.delete(webSocket);
1075
- }
1076
- /** Dispose any live runtime before the host deletes its Durable Object storage. */
1077
- async destroy(_input) {
894
+ async destroy() {
1078
895
  await this.keepAlive.shutdown();
1079
896
  await this.dispose();
1080
- this.runtimeState = void 0;
1081
- this.runtimePromise = void 0;
897
+ this.state = void 0;
898
+ this.building = void 0;
1082
899
  }
1083
- /** Durable Object alarm hook: wake/recover the runtime and re-arm while EDA has active work. */
1084
- async alarm(input) {
1085
- const sessionId = input?.sessionId ?? this.runtimeState?.sessionId;
1086
- if (sessionId !== void 0) await this.getRuntime(sessionId);
1087
- await this.keepAlive.alarm();
1088
- }
1089
- /** Dispose the Effect runtime if the host wrapper is torn down by tests. */
1090
900
  async dispose() {
1091
- await this.runtimeState?.runtime.dispose();
1092
- }
1093
- async withRuntime(sessionId, f) {
1094
- return await (await this.getRuntime(sessionId)).runtime.runPromise(Effect.gen(function* () {
1095
- return yield* f(yield* EDARuntime);
1096
- }));
1097
- }
1098
- async ensureEventWebSocket(webSocket) {
1099
- const existing = this.eventSockets.get(webSocket);
1100
- if (existing !== void 0) return existing;
1101
- const decoded = await decodeWebSocketAttachment(webSocket.deserializeAttachment());
1102
- if (decoded._tag !== "Decoded") return;
1103
- await this.startEventWebSocket(webSocket, decoded.attachment);
1104
- return this.eventSockets.get(webSocket);
901
+ await this.state?.runtime.dispose();
1105
902
  }
1106
- async startEventWebSocket(webSocket, attachment) {
1107
- if (this.eventSockets.has(webSocket)) return;
1108
- const runtime = await this.getRuntime(attachment.sessionId);
1109
- const state = {
1110
- ...await runtime.runtime.runPromise(Effect.gen(function* () {
1111
- return {
1112
- incoming: yield* Queue.unbounded(),
1113
- closed: yield* Deferred.make()
1114
- };
1115
- })),
1116
- interrupt: void 0,
1117
- lastAckedSeq: attachment.lastAckedSeq,
1118
- sessionId: attachment.sessionId,
1119
- subscriberId: attachment.subscriberId
1120
- };
1121
- this.eventSockets.set(webSocket, state);
1122
- const send = (frame) => Effect.try({
1123
- try: () => webSocket.send((0, websocket_protocol_exports.encodeEDAWebSocketServerFrame)(frame, this.options.webSocketProtocol)),
1124
- catch: (cause) => new SubscriberSendFailed({
1125
- subscriberId: state.subscriberId,
1126
- cause
1127
- })
1128
- });
1129
- const persistAck = (seq) => Effect.sync(() => {
1130
- state.lastAckedSeq = seq;
1131
- webSocket.serializeAttachment({
1132
- kind: "eda-events-v1",
1133
- sessionId: state.sessionId,
1134
- subscriberId: state.subscriberId,
1135
- lastAckedSeq: seq,
1136
- trace: attachment.trace
1137
- });
1138
- });
1139
- const closeForError = (error) => {
1140
- if (error instanceof SubscriberLagged || isTagged(error, "SubscriberLagged")) {
1141
- const lagged = error;
1142
- return send(websocket_protocol_exports.EDAWebSocketLaggedFrame.make({
1143
- _tag: "lagged",
1144
- resumeSeq: lagged.lastAckedSeq,
1145
- reason: lagged.reason
1146
- })).pipe(Effect.ignore, Effect.andThen(Effect.sync(() => closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_LAGGED, (0, websocket_protocol_exports.laggedCloseReason)(lagged.lastAckedSeq)))));
1147
- }
1148
- if (error instanceof SubscriberProtocolError || isTagged(error, "SubscriberProtocolError")) {
1149
- const protocolError = error;
1150
- return send(websocket_protocol_exports.EDAWebSocketErrorFrame.make({
1151
- _tag: "error",
1152
- message: protocolError.message.slice(0, 120)
1153
- })).pipe(Effect.ignore, Effect.andThen(Effect.sync(() => closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol"))));
1154
- }
1155
- return Effect.sync(() => closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_SEND_FAILED, "send-failed"));
1156
- };
1157
- const program = Effect.scoped(runWebSocketSubscriber({
1158
- subscriberId: state.subscriberId,
1159
- resumeSeq: state.lastAckedSeq,
1160
- policy: websocket_protocol_exports.defaultEDAWebSocketFlowControl,
1161
- transport: {
1162
- send,
1163
- incoming: state.incoming,
1164
- closed: state.closed,
1165
- persistAck
1166
- }
1167
- }).pipe(Effect.catch((error) => closeForError(error)), Effect.ensuring(Effect.sync(() => {
1168
- this.eventSockets.delete(webSocket);
1169
- })), Effect.withSpan("agent.events.stream", hostSpanOptions(attachment.trace, {
1170
- "eda.session.id": state.sessionId,
1171
- "eda.subscriber.id": state.subscriberId,
1172
- "eda.seq.after": state.lastAckedSeq
1173
- }))));
1174
- state.interrupt = runtime.runtime.runCallback(program, { onExit: (exit) => {
1175
- this.eventSockets.delete(webSocket);
1176
- if (Exit.isFailure(exit)) closeWebSocket(webSocket, websocket_protocol_exports.EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
1177
- } });
1178
- }
1179
- async getRuntime(sessionId) {
1180
- if (this.runtimeState !== void 0) {
1181
- if (this.runtimeState.sessionId !== sessionId) throw new Error(`EDASessionDurableObjectHost is scoped to session ${this.runtimeState.sessionId}; received ${sessionId}`);
1182
- return this.runtimeState;
903
+ async get(sessionId) {
904
+ if (this.state !== void 0) {
905
+ if (this.state.sessionId !== sessionId) throw new Error(`EDA session runtime is scoped to session ${this.state.sessionId}; received ${sessionId}`);
906
+ return this.state;
1183
907
  }
1184
908
  this.keepAlive.restart();
1185
- const runtimePromise = this.runtimePromise ?? this.buildRuntime(sessionId);
1186
- this.runtimePromise = runtimePromise;
909
+ const building = this.building ?? this.build(sessionId);
910
+ this.building = building;
1187
911
  try {
1188
- return await runtimePromise;
912
+ return await building;
1189
913
  } catch (error) {
1190
- if (this.runtimePromise === runtimePromise) this.runtimePromise = void 0;
914
+ if (this.building === building) this.building = void 0;
1191
915
  throw error;
1192
916
  }
1193
917
  }
1194
- async buildRuntime(sessionId) {
918
+ async build(sessionId) {
1195
919
  const runtime = ManagedRuntime.make(makeEDADurableObjectRuntimeLayer({
1196
920
  config: this.options.config,
1197
921
  compactionExecutorLayer: this.options.compactionExecutorLayer,
@@ -1200,12 +924,13 @@ var EDASessionDurableObjectHost = class {
1200
924
  modelLayer: this.options.modelLayer,
1201
925
  promptProjectorLayer: this.options.promptProjectorLayer,
1202
926
  reducers: this.options.reducers,
927
+ sessionEventObserverLayer: this.options.sessionEventObserverLayer,
1203
928
  sessionId,
1204
929
  sinks: this.options.sinks,
1205
930
  storage: this.options.storage,
1206
- toolkit: resolveRuntimeFactory(this.options.toolkit, sessionId),
1207
- toolRegistry: resolveRuntimeFactory(this.options.toolRegistry, sessionId),
1208
- tracer: resolveRuntimeFactory(this.options.tracer, sessionId)
931
+ toolkit: resolveFactory(this.options.toolkit, sessionId),
932
+ toolRegistry: resolveFactory(this.options.toolRegistry, sessionId),
933
+ tracer: resolveFactory(this.options.tracer, sessionId)
1209
934
  }));
1210
935
  const state = {
1211
936
  runtime,
@@ -1213,7 +938,7 @@ var EDASessionDurableObjectHost = class {
1213
938
  };
1214
939
  try {
1215
940
  await runtime.context();
1216
- this.runtimeState = state;
941
+ this.state = state;
1217
942
  return state;
1218
943
  } catch (error) {
1219
944
  await runtime.dispose().catch(() => void 0);
@@ -1221,35 +946,487 @@ var EDASessionDurableObjectHost = class {
1221
946
  }
1222
947
  }
1223
948
  };
949
+ const resolveFactory = (value, sessionId) => typeof value === "function" ? value({ sessionId }) : value;
950
+ //#endregion
951
+ //#region src/websocket/projection.ts
952
+ /** Stable non-empty identity persisted with an app WebSocket projection. */
953
+ const EDAWebSocketProjectionId = Schema.NonEmptyString.pipe(Schema.brand("EDAWebSocketProjectionId"));
954
+ //#endregion
955
+ //#region src/websocket/attachment.ts
956
+ /** Versioned state required to resume strict delivery after Durable Object hibernation. */
957
+ const EDAWebSocketAttachment = Schema.Struct({
958
+ kind: Schema.Literal("eda-events-v2"),
959
+ sessionId: SessionId,
960
+ subscriberId: SubscriberId,
961
+ trace: EDATraceMetadata,
962
+ delivery: EDAWebSocketDeliveryCheckpoint,
963
+ projection: Schema.optionalKey(Schema.Struct({
964
+ id: EDAWebSocketProjectionId,
965
+ state: Schema.Unknown
966
+ }))
967
+ });
968
+ /** Decode and classify an attachment read from Cloudflare's structured-clone boundary. */
1224
969
  const decodeWebSocketAttachment = async (input) => {
1225
970
  if (input === null || input === void 0) return { _tag: "Missing" };
1226
971
  try {
1227
972
  return {
1228
973
  _tag: "Decoded",
1229
- attachment: await Effect.runPromise((0, websocket_protocol_exports.decodeEDAWebSocketAttachment)(input))
974
+ attachment: await Effect.runPromise(Schema.decodeUnknownEffect(EDAWebSocketAttachment)(input))
1230
975
  };
1231
976
  } catch {
1232
977
  return { _tag: "Malformed" };
1233
978
  }
1234
979
  };
1235
- const closeWebSocket = (webSocket, code, reason) => {
1236
- try {
1237
- webSocket.close(code, reason);
1238
- } catch {}
980
+ /** Encode before serialization so schema transformations cannot leak into attachments. */
981
+ const encodeWebSocketAttachment = (attachment) => Schema.encodeSync(EDAWebSocketAttachment)(attachment);
982
+ //#endregion
983
+ //#region src/websocket/connection-manager.ts
984
+ /**
985
+ * Owns Cloudflare WebSockets, hibernation attachments, and interpretation of
986
+ * the platform-neutral delivery state machine.
987
+ */
988
+ var EDAWebSocketConnectionManager = class {
989
+ options;
990
+ 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 }))));
991
+ sockets = /* @__PURE__ */ new Map();
992
+ constructor(options) {
993
+ this.options = options;
994
+ }
995
+ resolveProjection(id) {
996
+ if (id === void 0) return void 0;
997
+ if (this.options.webSocketProjection?.id === id) return this.options.webSocketProjection;
998
+ }
999
+ async accept(input) {
1000
+ const subscriberId = SubscriberId.make(crypto.randomUUID());
1001
+ const delivery = makeWebSocketDeliveryState({
1002
+ subscriberId,
1003
+ resumeSeq: input.afterSeq,
1004
+ policy: defaultEDAWebSocketFlowControl
1005
+ });
1006
+ const state = {
1007
+ sessionId: input.sessionId,
1008
+ subscriberId,
1009
+ trace: input.trace,
1010
+ delivery,
1011
+ catchUpDeferred: false,
1012
+ head: input.afterSeq,
1013
+ queue: Promise.resolve(),
1014
+ ...input.projectionState === void 0 ? {} : { projectionState: input.projectionState }
1015
+ };
1016
+ this.sockets.set(input.webSocket, state);
1017
+ if (!this.persist(input.webSocket, state)) return;
1018
+ if (!await this.send(input.webSocket, state, [deliveryHelloFrame(delivery)])) return;
1019
+ if (!await this.prepare(input.webSocket, state)) return;
1020
+ await this.enqueue(input.webSocket, state, async () => {
1021
+ await this.readAndDeliver(input.webSocket, state, input.afterSeq);
1022
+ });
1023
+ }
1024
+ async message(webSocket, message) {
1025
+ if (typeof message !== "string") {
1026
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "binary");
1027
+ return;
1028
+ }
1029
+ if (message === EDA_WEB_SOCKET_PING_MESSAGE) {
1030
+ try {
1031
+ webSocket.send(EDA_WEB_SOCKET_PONG_MESSAGE);
1032
+ } catch {
1033
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "send-failed");
1034
+ }
1035
+ return;
1036
+ }
1037
+ const state = await this.restore(webSocket);
1038
+ if (state === void 0) {
1039
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1040
+ return;
1041
+ }
1042
+ const frame = await Effect.runPromise(this.decodeClientMessage(message, state).pipe(Effect.option));
1043
+ if (frame._tag === "None") {
1044
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1045
+ return;
1046
+ }
1047
+ const clientFrame = frame.value;
1048
+ if (clientFrame._tag === "ping") return;
1049
+ if (!await this.prepare(webSocket, state)) return;
1050
+ await this.enqueue(webSocket, state, async () => {
1051
+ await this.interpret(webSocket, state, onClientAck(state.delivery, clientFrame, Date.now()));
1052
+ });
1053
+ }
1054
+ closed(webSocket) {
1055
+ this.sockets.delete(webSocket);
1056
+ }
1057
+ async flushDeferredCatchUp(sessionId) {
1058
+ const deliveries = [];
1059
+ for (const [webSocket, state] of this.sockets) {
1060
+ if (state.sessionId !== sessionId || !state.catchUpDeferred) continue;
1061
+ deliveries.push(this.enqueue(webSocket, state, async () => {
1062
+ if (!state.catchUpDeferred) return;
1063
+ state.catchUpDeferred = false;
1064
+ await this.readAndDeliver(webSocket, state, state.delivery.sentDurableThroughSeq);
1065
+ }));
1066
+ }
1067
+ await Promise.all(deliveries);
1068
+ }
1069
+ async fanoutPublishedEvent(event) {
1070
+ const webSockets = new Set([...this.options.getWebSockets?.() ?? [], ...this.sockets.keys()]);
1071
+ const deliveries = [];
1072
+ for (const webSocket of webSockets) {
1073
+ const state = await this.restore(webSocket);
1074
+ if (state === void 0) continue;
1075
+ deliveries.push(this.enqueue(webSocket, state, async () => {
1076
+ if (event.event.durability !== "durable") {
1077
+ await this.interpret(webSocket, state, onEphemeralEvent(state.delivery, event, Date.now()));
1078
+ return;
1079
+ }
1080
+ state.head = SequenceNumber.make(Math.max(state.head, event.position.seq));
1081
+ if (event.position.seq <= state.delivery.sentDurableThroughSeq) return;
1082
+ if (event.position.seq === state.delivery.sentDurableThroughSeq + 1) {
1083
+ await this.interpret(webSocket, state, onDurableEvents(state.delivery, [event], state.head, Date.now()));
1084
+ return;
1085
+ }
1086
+ if (!this.options.isSessionReady(state.sessionId)) {
1087
+ state.catchUpDeferred = true;
1088
+ return;
1089
+ }
1090
+ await this.readAndDeliver(webSocket, state, state.delivery.sentDurableThroughSeq);
1091
+ }));
1092
+ }
1093
+ await Promise.all(deliveries);
1094
+ }
1095
+ async restore(webSocket) {
1096
+ const existing = this.sockets.get(webSocket);
1097
+ if (existing !== void 0) return existing;
1098
+ const decoded = await decodeWebSocketAttachment(webSocket.deserializeAttachment());
1099
+ if (decoded._tag !== "Decoded") {
1100
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1101
+ return;
1102
+ }
1103
+ const attachment = decoded.attachment;
1104
+ const projectionState = attachment.projection === void 0 ? void 0 : await this.decodeProjectionState(attachment.projection.id, attachment.projection.state);
1105
+ if (attachment.projection !== void 0 && projectionState === void 0) {
1106
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1107
+ return;
1108
+ }
1109
+ const state = {
1110
+ sessionId: attachment.sessionId,
1111
+ subscriberId: attachment.subscriberId,
1112
+ trace: attachment.trace,
1113
+ delivery: restoreWebSocketDeliveryState({
1114
+ subscriberId: attachment.subscriberId,
1115
+ policy: defaultEDAWebSocketFlowControl,
1116
+ checkpoint: attachment.delivery
1117
+ }),
1118
+ catchUpDeferred: false,
1119
+ head: attachment.delivery.sentDurableThroughSeq,
1120
+ queue: Promise.resolve(),
1121
+ ...projectionState === void 0 ? {} : { projectionState }
1122
+ };
1123
+ this.sockets.set(webSocket, state);
1124
+ return state;
1125
+ }
1126
+ async prepare(webSocket, state) {
1127
+ try {
1128
+ await this.options.prepareSession(state.sessionId);
1129
+ return this.sockets.get(webSocket) === state;
1130
+ } catch (error) {
1131
+ await Effect.runPromise(Effect.logError("EDA WebSocket session preparation failed", {
1132
+ error,
1133
+ subscriberId: state.subscriberId
1134
+ }));
1135
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
1136
+ return false;
1137
+ }
1138
+ }
1139
+ async interpret(webSocket, state, result) {
1140
+ state.delivery = result.state;
1141
+ for (const action of result.actions) switch (action._tag) {
1142
+ case "Persist":
1143
+ if (!this.persist(webSocket, state)) return false;
1144
+ break;
1145
+ case "Send":
1146
+ if (!await this.send(webSocket, state, action.frames)) return false;
1147
+ break;
1148
+ case "ReadEventPage":
1149
+ if (!await this.readAndDeliver(webSocket, state, action.afterSeq)) return false;
1150
+ break;
1151
+ case "Close":
1152
+ this.close(webSocket, action.code, action.reason);
1153
+ return false;
1154
+ }
1155
+ return true;
1156
+ }
1157
+ async readAndDeliver(webSocket, state, afterSeq) {
1158
+ const page = await this.options.readEventPage({
1159
+ afterSeq,
1160
+ limit: state.delivery.policy.maxInFlightFrames * state.delivery.policy.maxFrameEvents,
1161
+ sessionId: state.sessionId,
1162
+ subscriberId: state.subscriberId,
1163
+ trace: state.trace
1164
+ });
1165
+ state.head = SequenceNumber.make(Math.max(state.head, page.head));
1166
+ if (page.events.length === 0) return true;
1167
+ return await this.interpret(webSocket, state, onDurableEvents(state.delivery, page.events, state.head, Date.now()));
1168
+ }
1169
+ persist(webSocket, state) {
1170
+ try {
1171
+ const projection = state.projectionState === void 0 ? void 0 : this.options.webSocketProjection;
1172
+ if (state.projectionState !== void 0 && projection === void 0) {
1173
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1174
+ return false;
1175
+ }
1176
+ const attachment = EDAWebSocketAttachment.make({
1177
+ kind: "eda-events-v2",
1178
+ sessionId: state.sessionId,
1179
+ subscriberId: state.subscriberId,
1180
+ trace: state.trace,
1181
+ delivery: checkpointWebSocketDeliveryState(state.delivery),
1182
+ ...state.projectionState === void 0 || projection === void 0 ? {} : { projection: {
1183
+ id: projection.id,
1184
+ state: projection.encodeState(state.projectionState)
1185
+ } }
1186
+ });
1187
+ webSocket.serializeAttachment(encodeWebSocketAttachment(attachment));
1188
+ return true;
1189
+ } catch {
1190
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "attachment-failed");
1191
+ return false;
1192
+ }
1193
+ }
1194
+ send(webSocket, state, frames) {
1195
+ return this.sendPrepared(webSocket, state, frames);
1196
+ }
1197
+ async sendPrepared(webSocket, state, frames) {
1198
+ try {
1199
+ const encodedFrames = [];
1200
+ const followUpActions = [];
1201
+ for (const frame of frames) {
1202
+ const projected = state.projectionState === void 0 ? void 0 : this.options.webSocketProjection?.encodeServerFrame(frame, state.projectionState);
1203
+ if (state.projectionState !== void 0 && projected === void 0) {
1204
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1205
+ return false;
1206
+ }
1207
+ if (projected?._tag === "SuppressAndAck") {
1208
+ if (frame._tag !== "events") {
1209
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
1210
+ return false;
1211
+ }
1212
+ state.projectionState = projected.state;
1213
+ const acknowledged = onHostSuppressedFrame(state.delivery, {
1214
+ durableThroughSeq: frame.durableThroughSeq,
1215
+ frameId: frame.frameId
1216
+ }, Date.now());
1217
+ state.delivery = acknowledged.state;
1218
+ followUpActions.push(...acknowledged.actions.filter((action) => action._tag !== "Persist"));
1219
+ continue;
1220
+ }
1221
+ if (projected !== void 0) state.projectionState = projected.state;
1222
+ const encoded = projected?._tag === "Send" ? projected.frame : encodeEDAWebSocketServerFrame(frame, this.options.webSocketProtocol);
1223
+ if (new TextEncoder().encode(encoded).byteLength > state.delivery.policy.maxFrameBytes) {
1224
+ this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "frame-too-large");
1225
+ return false;
1226
+ }
1227
+ encodedFrames.push(encoded);
1228
+ }
1229
+ if (state.projectionState !== void 0 && !this.persist(webSocket, state)) return false;
1230
+ for (const encoded of encodedFrames) webSocket.send(encoded);
1231
+ if (followUpActions.length > 0) return await this.interpret(webSocket, state, {
1232
+ state: state.delivery,
1233
+ actions: coalesceFollowUpActions(followUpActions)
1234
+ });
1235
+ return true;
1236
+ } catch {
1237
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "send-failed");
1238
+ return false;
1239
+ }
1240
+ }
1241
+ enqueue(webSocket, state, operation) {
1242
+ const run = async () => {
1243
+ try {
1244
+ await operation();
1245
+ } catch (error) {
1246
+ await Effect.runPromise(Effect.logError("EDA WebSocket operation failed", {
1247
+ error,
1248
+ subscriberId: state.subscriberId
1249
+ }));
1250
+ this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
1251
+ }
1252
+ };
1253
+ const next = state.queue.then(run);
1254
+ state.queue = next;
1255
+ return next;
1256
+ }
1257
+ close(webSocket, code, reason) {
1258
+ this.sockets.delete(webSocket);
1259
+ try {
1260
+ webSocket.close(code, reason);
1261
+ } catch {}
1262
+ }
1263
+ decodeClientMessage(message, state) {
1264
+ return state.projectionState === void 0 ? decodeEDAWebSocketClientMessage(message) : this.options.webSocketProjection?.decodeClientMessage(message) ?? Effect.fail("protocol");
1265
+ }
1266
+ async decodeProjectionState(id, encoded) {
1267
+ const projection = this.resolveProjection(id);
1268
+ if (projection === void 0) return void 0;
1269
+ const decoded = await Effect.runPromise(projection.decodeState(encoded).pipe(Effect.option));
1270
+ return decoded._tag === "Some" ? decoded.value : void 0;
1271
+ }
1239
1272
  };
1240
- const isTagged = (input, tag) => typeof input === "object" && input !== null && "_tag" in input && input._tag === tag;
1241
- /** Decode a raw RPC command payload at the Durable Object boundary. */
1242
- const encodeEdaRpcCommand = (input) => Schema.encodeSync(EDACommand)(input);
1243
- const EDARpcSubmittable = Schema.Union([EDACommand, DurableEventEnvelope]);
1244
- /** Encode a command/app-event batch into structured-clone-safe RPC values. */
1245
- const encodeEdaRpcSubmittables = (input) => input.map((item) => Schema.encodeSync(EDARpcSubmittable)(item));
1246
- /** Decode a raw RPC command payload at the Durable Object boundary. */
1247
- const decodeEdaRpcCommand = (input) => Schema.decodeUnknownEffect(EDACommand)(input);
1248
- /** Decode a raw RPC batch item at the Durable Object boundary. */
1249
- const decodeEdaRpcSubmittable = (input) => Schema.decodeUnknownEffect(EDARpcSubmittable)(input);
1250
- /** Decode raw RPC batch items at the Durable Object boundary. */
1251
- const decodeEdaRpcSubmittables = (input) => Effect.forEach(input, decodeEdaRpcSubmittable);
1273
+ const coalesceFollowUpActions = (actions) => {
1274
+ let readAfterSeq;
1275
+ const retained = [];
1276
+ for (const action of actions) {
1277
+ if (action._tag !== "ReadEventPage") {
1278
+ retained.push(action);
1279
+ continue;
1280
+ }
1281
+ readAfterSeq = SequenceNumber.make(Math.max(readAfterSeq ?? 0, action.afterSeq));
1282
+ }
1283
+ return readAfterSeq === void 0 ? retained : [...retained, {
1284
+ _tag: "ReadEventPage",
1285
+ afterSeq: readAfterSeq
1286
+ }];
1287
+ };
1288
+ //#endregion
1289
+ //#region src/session-controller.ts
1290
+ /**
1291
+ * Public application-facing use cases for one EDA session.
1292
+ *
1293
+ * Runtime lifecycle belongs to EDASessionRuntime; WebSocket transport belongs
1294
+ * to EDAWebSocketConnectionManager. This class only coordinates them.
1295
+ */
1296
+ var EDASessionController = class {
1297
+ runtime;
1298
+ webSockets;
1299
+ constructor(options) {
1300
+ this.webSockets = new EDAWebSocketConnectionManager({
1301
+ getWebSockets: options.getWebSockets,
1302
+ isSessionReady: (sessionId) => this.runtime.isReady(sessionId),
1303
+ prepareSession: (sessionId) => this.prepareSession(sessionId),
1304
+ 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, {
1305
+ "eda.session.id": sessionId,
1306
+ "eda.subscriber.id": subscriberId,
1307
+ "eda.seq.after": afterSeq
1308
+ })))),
1309
+ webSocketProjection: options.webSocketProjection,
1310
+ webSocketProtocol: options.webSocketProtocol
1311
+ });
1312
+ this.runtime = new EDASessionRuntime({
1313
+ ...options,
1314
+ sessionEventObserverLayer: this.webSockets.observerLayer
1315
+ });
1316
+ }
1317
+ static migrate = EDASessionRuntime.migrate;
1318
+ submit(input) {
1319
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1320
+ return this.run(input.sessionId, (eda) => eda.submit(input.command).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
1321
+ "eda.session.id": input.sessionId,
1322
+ "eda.command.kind": input.command._tag,
1323
+ "eda.command.id": input.command.commandId
1324
+ }))));
1325
+ }
1326
+ submitBatch(input) {
1327
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1328
+ return this.run(input.sessionId, (eda) => eda.submit(input.items).pipe(Effect.withSpan("agent.command.submit", hostSpanOptions(trace, {
1329
+ "eda.session.id": input.sessionId,
1330
+ "eda.command.submittable_count": input.items.length
1331
+ }))));
1332
+ }
1333
+ submitAndBlock(input) {
1334
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1335
+ return this.run(input.sessionId, (eda) => eda.submitAndBlock(input.command).pipe(Effect.withSpan("agent.command.submit.wait", hostSpanOptions(trace, {
1336
+ "eda.session.id": input.sessionId,
1337
+ "eda.command.kind": input.command._tag,
1338
+ "eda.command.id": input.command.commandId
1339
+ }))));
1340
+ }
1341
+ blockOnCommand(input) {
1342
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1343
+ return this.run(input.sessionId, (eda) => eda.blockOnCommand(input.commandId, input.afterSeq ?? SequenceNumber.make(0)).pipe(Effect.withSpan("agent.command.wait", hostSpanOptions(trace, {
1344
+ "eda.session.id": input.sessionId,
1345
+ "eda.command.id": input.commandId,
1346
+ "eda.seq.after": input.afterSeq
1347
+ }))));
1348
+ }
1349
+ snapshot(input) {
1350
+ return this.query(input, (eda) => eda.snapshot(), "agent.session.snapshot");
1351
+ }
1352
+ messages(input) {
1353
+ return this.query(input, (eda) => eda.messages(), "agent.messages.list");
1354
+ }
1355
+ acceptEventWebSocket(input) {
1356
+ return this.acceptEventWebSocketUnhosted(input);
1357
+ }
1358
+ async acceptEventWebSocketUnhosted(input) {
1359
+ const prepared = await this.prepareEventWebSocket(input);
1360
+ await this.acceptPreparedEventWebSocket({
1361
+ ...prepared,
1362
+ webSocket: input.webSocket
1363
+ });
1364
+ }
1365
+ async prepareEventWebSocket(input) {
1366
+ const projection = this.webSockets.resolveProjection(input.projectionId);
1367
+ if (input.projectionId !== void 0 && projection === void 0) throw new Error(`Unsupported EDA WebSocket projection: ${input.projectionId}`);
1368
+ const initialized = projection === void 0 ? void 0 : projection.initialize({
1369
+ ...input.afterSeq === void 0 ? {} : { requestedAfterSeq: input.afterSeq },
1370
+ snapshot: await this.snapshot({
1371
+ sessionId: input.sessionId,
1372
+ trace: input.trace
1373
+ })
1374
+ });
1375
+ return {
1376
+ afterSeq: initialized?.afterSeq ?? input.afterSeq ?? SequenceNumber.make(0),
1377
+ sessionId: input.sessionId,
1378
+ trace: input.trace ?? makeRootEDATraceMetadata(),
1379
+ ...initialized === void 0 ? {} : { projectionState: initialized.state }
1380
+ };
1381
+ }
1382
+ acceptPreparedEventWebSocket(input) {
1383
+ return this.webSockets.accept(input);
1384
+ }
1385
+ webSocketMessage(webSocket, message) {
1386
+ return this.webSockets.message(webSocket, message);
1387
+ }
1388
+ webSocketClose(webSocket) {
1389
+ this.webSockets.closed(webSocket);
1390
+ }
1391
+ webSocketError(webSocket) {
1392
+ this.webSockets.closed(webSocket);
1393
+ }
1394
+ async destroy(_input) {
1395
+ await this.runtime.destroy();
1396
+ }
1397
+ async alarm(input) {
1398
+ if (input !== void 0) await this.prepareSession(input.sessionId);
1399
+ await this.runtime.alarm(input?.sessionId);
1400
+ }
1401
+ dispose() {
1402
+ return this.runtime.dispose();
1403
+ }
1404
+ query(input, operation, spanName) {
1405
+ const trace = input.trace ?? makeRootEDATraceMetadata();
1406
+ return this.run(input.sessionId, (eda) => operation(eda).pipe(Effect.withSpan(spanName, hostSpanOptions(trace, { "eda.session.id": input.sessionId }))));
1407
+ }
1408
+ async prepareSession(sessionId) {
1409
+ await this.runtime.prepare(sessionId);
1410
+ await this.webSockets.flushDeferredCatchUp(sessionId);
1411
+ }
1412
+ async run(sessionId, operation) {
1413
+ await this.prepareSession(sessionId);
1414
+ return await this.runtime.run(sessionId, operation);
1415
+ }
1416
+ };
1417
+ const hostSpanOptions = (trace, attributes) => ({
1418
+ kind: "server",
1419
+ attributes: compactSpanAttributes({
1420
+ ...trace.attributes,
1421
+ ...attributes
1422
+ }),
1423
+ links: trace.links.map((link) => ({
1424
+ span: toEdaExternalSpan(link.context),
1425
+ attributes: link.attributes
1426
+ })),
1427
+ ...trace.parent === null ? {} : { parent: toEdaExternalSpan(trace.parent) }
1428
+ });
1252
1429
  //#endregion
1253
- export { encodeEdaRpcCommand as a, makeEDADurableObjectRuntimeLayer as c, decodeEdaRpcSubmittables as i, decodeEdaRpcCommand as n, encodeEdaRpcSubmittables as o, decodeEdaRpcSubmittable as r, makeEDADurableObjectOpenAiModelLayer as s, EDASessionDurableObjectHost as t };
1430
+ export { EDAWebSocketProjectionId as n, makeEDADurableObjectRuntimeLayer as r, EDASessionController as t };
1254
1431
 
1255
- //# sourceMappingURL=durable-object-runtime-LgOJbsVm.js.map
1432
+ //# sourceMappingURL=session-controller-CMbW7vgf.js.map