effect-durable-agent-cloudflare 0.1.0-alpha.22 → 0.1.0-alpha.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/durable-object-BZJM3RyV.js +477 -0
- package/dist/durable-object-BZJM3RyV.js.map +1 -0
- package/dist/durable-object-storage-D3muKtpz.d.ts +115 -0
- package/dist/durable-object-store-Dct3N6rk.js +1148 -0
- package/dist/durable-object-store-Dct3N6rk.js.map +1 -0
- package/dist/durable-object.d.ts +59 -4
- package/dist/durable-object.js +2 -207
- package/dist/index.d.ts +181 -5
- package/dist/index.js +5 -4
- package/dist/rpc-spec-B5cL83zd.js +303 -0
- package/dist/rpc-spec-B5cL83zd.js.map +1 -0
- package/dist/{rpc-spec-C8wknrbN.d.ts → rpc-spec-BTVd02Rg.d.ts} +1718 -4
- package/dist/rpc.d.ts +2 -2
- package/dist/rpc.js +2 -2
- package/dist/{session-controller-DoeFP_Ej.d.ts → session-controller-D8uXDKVG.d.ts} +66 -6
- package/dist/{session-controller-BGfz1DdS.js → session-controller-DoUy9zR6.js} +637 -642
- package/dist/session-controller-DoUy9zR6.js.map +1 -0
- package/dist/session-controller.d.ts +1 -1
- package/dist/session-controller.js +1 -1
- package/dist/storage.d.ts +1 -1
- package/package.json +2 -2
- package/dist/durable-object-storage-BZCOHz9p.d.ts +0 -40
- package/dist/durable-object.js.map +0 -1
- package/dist/rpc-spec-CkWEeWJG.js +0 -105
- package/dist/rpc-spec-CkWEeWJG.js.map +0 -1
- package/dist/session-controller-BGfz1DdS.js.map +0 -1
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { ContextRebasedPayload, DurableEventEnvelope, SummaryCreatedPayload, assistantMessageCommittedEventType, assistantMessageImportedEventType, assistantPartialCommittedEventType, commandAdmittedEventType, commandCancelledEventType, commandCompletedEventType, commandFailedEventType, commandStartedEventType, contextRebasedEventType, decodeDurableEventSync, effectDurableAgentNamespace, steeringMessageQueuedEventType, summaryCreatedEventType, systemMessageCommittedEventType, userMessageCommittedEventType } from "effect-durable-agent/types/events";
|
|
5
|
+
import { SequenceNumber, SummaryId, durablePosition } from "effect-durable-agent/types/core";
|
|
6
|
+
import { EDASessionStore, EDASessionStoreError } from "effect-durable-agent/services/session-store";
|
|
7
|
+
import { TranscriptLogIncarnation, TranscriptRecordFromEnvelope, TranscriptRecordId, TranscriptVersion, isPendingTranscriptUserRecord, isTopLevelTranscriptRecord, planTranscriptBatch } from "effect-durable-agent/transcript";
|
|
8
|
+
import * as Cause from "effect/Cause";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as Stream from "effect/Stream";
|
|
11
|
+
import { CompactionSummaryArtifact } from "effect-durable-agent/domain/context-projection";
|
|
12
|
+
import { annotateEdaSpan, committedBatchAttributes } from "effect-durable-agent/services/tracing";
|
|
13
|
+
import { IdGenerator } from "effect-durable-agent/services/id-generator";
|
|
14
|
+
//#region src/durable-object-json.ts
|
|
15
|
+
/** Conservative JSON limit below the Durable Object SQLite per-value ceiling (2 MB). */
|
|
16
|
+
const durableObjectSerializedJsonHardCapBytes = 15e5;
|
|
17
|
+
const jsonEncoder = new TextEncoder();
|
|
18
|
+
/** Serialize and size-check one JSON column before it reaches DO SQLite. */
|
|
19
|
+
const stringifyJsonForColumn = (value, column) => {
|
|
20
|
+
const json = JSON.stringify(value);
|
|
21
|
+
if (json === void 0) throw new Error(`${column} value is not JSON serializable`);
|
|
22
|
+
const bytes = jsonEncoder.encode(json).byteLength;
|
|
23
|
+
if (bytes > 15e5) throw new Error(`${column} serialized JSON is ${bytes} bytes; hard cap is ${durableObjectSerializedJsonHardCapBytes} bytes`);
|
|
24
|
+
return json;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/durable-object-transcript-tables.ts
|
|
28
|
+
/**
|
|
29
|
+
* SQL layout of the transcript projection (design §5.1), created by the store's forward migration.
|
|
30
|
+
*
|
|
31
|
+
* Rows are keyed by a **slot**: one stored instance of one transcript version. A version is built
|
|
32
|
+
* in a fresh slot, so a retired slot that is still being deleted in bounded slices never collides
|
|
33
|
+
* with a later slot of the same version. At most one slot is `active` and at most one is
|
|
34
|
+
* `building`; `retired` slots are garbage awaiting cleanup and are never read.
|
|
35
|
+
*
|
|
36
|
+
* `_eda_log_incarnation` holds the identity of this physical event log (§8); `_eda_alarm_wakeups`
|
|
37
|
+
* holds each alarm owner's durable wakeup (see `durable-object-alarms.ts`).
|
|
38
|
+
*/
|
|
39
|
+
const transcriptTableStatements = [
|
|
40
|
+
`CREATE TABLE IF NOT EXISTS _eda_transcript_meta (
|
|
41
|
+
slot INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
version TEXT NOT NULL,
|
|
43
|
+
role TEXT NOT NULL CHECK (role IN ('active', 'building', 'retired')),
|
|
44
|
+
cursor INTEGER NOT NULL,
|
|
45
|
+
status TEXT NOT NULL CHECK (status IN ('ok', 'failed')),
|
|
46
|
+
failed_at_seq INTEGER,
|
|
47
|
+
error TEXT
|
|
48
|
+
)`,
|
|
49
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS _eda_transcript_meta_role_idx
|
|
50
|
+
ON _eda_transcript_meta(role) WHERE role IN ('active', 'building')`,
|
|
51
|
+
`CREATE TABLE IF NOT EXISTS _eda_transcript_records (
|
|
52
|
+
slot INTEGER NOT NULL,
|
|
53
|
+
id TEXT NOT NULL,
|
|
54
|
+
kind TEXT NOT NULL,
|
|
55
|
+
anchor_seq INTEGER,
|
|
56
|
+
ordinal INTEGER,
|
|
57
|
+
top_level INTEGER NOT NULL,
|
|
58
|
+
pending INTEGER NOT NULL,
|
|
59
|
+
assistant_id TEXT,
|
|
60
|
+
updated_seq INTEGER NOT NULL,
|
|
61
|
+
envelope_json TEXT NOT NULL,
|
|
62
|
+
PRIMARY KEY (slot, id)
|
|
63
|
+
)`,
|
|
64
|
+
`CREATE INDEX IF NOT EXISTS _eda_transcript_records_order_idx
|
|
65
|
+
ON _eda_transcript_records(slot, top_level, anchor_seq, ordinal, id)`,
|
|
66
|
+
`CREATE INDEX IF NOT EXISTS _eda_transcript_records_anchor_idx
|
|
67
|
+
ON _eda_transcript_records(slot, anchor_seq, ordinal) WHERE anchor_seq IS NOT NULL`,
|
|
68
|
+
`CREATE INDEX IF NOT EXISTS _eda_transcript_records_updated_idx
|
|
69
|
+
ON _eda_transcript_records(slot, updated_seq)`,
|
|
70
|
+
`CREATE INDEX IF NOT EXISTS _eda_transcript_records_pending_idx
|
|
71
|
+
ON _eda_transcript_records(slot, id) WHERE pending = 1`,
|
|
72
|
+
`CREATE INDEX IF NOT EXISTS _eda_transcript_records_tool_idx
|
|
73
|
+
ON _eda_transcript_records(slot, assistant_id) WHERE assistant_id IS NOT NULL`,
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS _eda_transcript_aliases (
|
|
75
|
+
slot INTEGER NOT NULL,
|
|
76
|
+
namespace TEXT NOT NULL,
|
|
77
|
+
key TEXT NOT NULL,
|
|
78
|
+
record_id TEXT NOT NULL,
|
|
79
|
+
PRIMARY KEY (slot, namespace, key)
|
|
80
|
+
)`,
|
|
81
|
+
`CREATE TABLE IF NOT EXISTS _eda_log_incarnation (
|
|
82
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
83
|
+
incarnation TEXT NOT NULL
|
|
84
|
+
)`,
|
|
85
|
+
`CREATE TABLE IF NOT EXISTS _eda_alarm_wakeups (
|
|
86
|
+
owner TEXT PRIMARY KEY,
|
|
87
|
+
at_ms INTEGER NOT NULL
|
|
88
|
+
)`
|
|
89
|
+
];
|
|
90
|
+
const decodeVersion = Schema.decodeUnknownSync(TranscriptVersion);
|
|
91
|
+
const decodeSlot = (row) => {
|
|
92
|
+
if (row.role !== "active" && row.role !== "building" && row.role !== "retired") throw new Error(`transcript slot ${row.slot} has unknown role ${row.role}`);
|
|
93
|
+
if (row.status === "failed" && (row.failed_at_seq === null || row.error === null)) throw new Error(`failed transcript slot ${row.slot} has no failure seq or error`);
|
|
94
|
+
return {
|
|
95
|
+
slot: row.slot,
|
|
96
|
+
version: decodeVersion(row.version),
|
|
97
|
+
role: row.role,
|
|
98
|
+
cursor: SequenceNumber.make(row.cursor),
|
|
99
|
+
status: row.status === "failed" ? {
|
|
100
|
+
_tag: "Failed",
|
|
101
|
+
failedAtSeq: SequenceNumber.make(row.failed_at_seq ?? 0),
|
|
102
|
+
error: row.error ?? ""
|
|
103
|
+
} : { _tag: "Ok" }
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/** Every stored slot, in creation order. */
|
|
107
|
+
const readTranscriptSlots = (sql) => sql.exec("SELECT slot, version, role, cursor, status, failed_at_seq, error FROM _eda_transcript_meta ORDER BY slot").toArray().map(decodeSlot);
|
|
108
|
+
/** The port's view of a slot; retired slots have none. */
|
|
109
|
+
const transcriptVersionMetaOf = (slot) => slot.role === "retired" ? void 0 : {
|
|
110
|
+
version: slot.version,
|
|
111
|
+
role: slot.role,
|
|
112
|
+
cursor: slot.cursor,
|
|
113
|
+
status: slot.status
|
|
114
|
+
};
|
|
115
|
+
const insertTranscriptSlot = (sql, version, role) => {
|
|
116
|
+
sql.exec("INSERT INTO _eda_transcript_meta (version, role, cursor, status) VALUES (?, ?, 0, 'ok')", version, role);
|
|
117
|
+
};
|
|
118
|
+
const setTranscriptSlotRole = (sql, slot, role) => {
|
|
119
|
+
sql.exec("UPDATE _eda_transcript_meta SET role = ? WHERE slot = ?", role, slot);
|
|
120
|
+
};
|
|
121
|
+
/** Freeze a slot at its unchanged cursor. A failed slot is never written again. */
|
|
122
|
+
const markTranscriptSlotFailed = (sql, slot, failedAtSeq, error) => {
|
|
123
|
+
sql.exec("UPDATE _eda_transcript_meta SET status = 'failed', failed_at_seq = ?, error = ? WHERE slot = ?", failedAtSeq, error, slot);
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Delete up to `limit` rows of a retired slot, records first, and the slot itself once empty.
|
|
127
|
+
* Returns the number of rows deleted.
|
|
128
|
+
*/
|
|
129
|
+
const deleteRetiredTranscriptRows = (sql, slot, limit) => {
|
|
130
|
+
const records = sql.exec(`DELETE FROM _eda_transcript_records WHERE rowid IN (
|
|
131
|
+
SELECT rowid FROM _eda_transcript_records WHERE slot = ? LIMIT ?
|
|
132
|
+
) RETURNING 1 AS deleted`, slot, limit).toArray().length;
|
|
133
|
+
const aliases = records >= limit ? 0 : sql.exec(`DELETE FROM _eda_transcript_aliases WHERE rowid IN (
|
|
134
|
+
SELECT rowid FROM _eda_transcript_aliases WHERE slot = ? LIMIT ?
|
|
135
|
+
) RETURNING 1 AS deleted`, slot, limit - records).toArray().length;
|
|
136
|
+
if (records + aliases < limit) sql.exec("DELETE FROM _eda_transcript_meta WHERE slot = ? AND role = 'retired'", slot);
|
|
137
|
+
return records + aliases;
|
|
138
|
+
};
|
|
139
|
+
const encodeEnvelope = Schema.encodeSync(TranscriptRecordFromEnvelope);
|
|
140
|
+
const decodeEnvelope = Schema.decodeUnknownSync(TranscriptRecordFromEnvelope);
|
|
141
|
+
/** A stored record row this release cannot decode. */
|
|
142
|
+
var TranscriptRowDecodeError = class extends Error {
|
|
143
|
+
name = "TranscriptRowDecodeError";
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Decode one stored row. A row this release cannot decode throws; maintenance treats that as a
|
|
147
|
+
* projection failure and reads as an unreadable window, never as a wrong view.
|
|
148
|
+
*/
|
|
149
|
+
const decodeRecordRow = (row) => {
|
|
150
|
+
try {
|
|
151
|
+
return decodeEnvelope(JSON.parse(row.envelope_json));
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
throw new TranscriptRowDecodeError(`undecodable transcript row: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const positionOf = (record) => "position" in record ? record.position : void 0;
|
|
157
|
+
const encodeTranscriptPlan = (plan) => ({
|
|
158
|
+
records: plan.records.map((record) => {
|
|
159
|
+
const position = positionOf(record);
|
|
160
|
+
return [
|
|
161
|
+
record.id,
|
|
162
|
+
record.kind,
|
|
163
|
+
position?.anchorSeq ?? null,
|
|
164
|
+
position?.ordinal ?? null,
|
|
165
|
+
isTopLevelTranscriptRecord(record) ? 1 : 0,
|
|
166
|
+
isPendingTranscriptUserRecord(record) ? 1 : 0,
|
|
167
|
+
record.kind === "tool" ? record.assistantId : null,
|
|
168
|
+
record.updatedSeq,
|
|
169
|
+
stringifyJsonForColumn(encodeEnvelope(record), "_eda_transcript_records.envelope_json")
|
|
170
|
+
];
|
|
171
|
+
}),
|
|
172
|
+
aliases: plan.aliases,
|
|
173
|
+
throughSeq: plan.throughSeq
|
|
174
|
+
});
|
|
175
|
+
/** Write an encoded plan to a slot and advance its cursor; the caller owns the transaction. */
|
|
176
|
+
const writeTranscriptPlan = (sql, slot, plan) => {
|
|
177
|
+
for (const row of plan.records) sql.exec(`INSERT INTO _eda_transcript_records (
|
|
178
|
+
slot, id, kind, anchor_seq, ordinal, top_level, pending, assistant_id, updated_seq, envelope_json
|
|
179
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
180
|
+
ON CONFLICT(slot, id) DO UPDATE SET
|
|
181
|
+
kind = excluded.kind,
|
|
182
|
+
anchor_seq = excluded.anchor_seq,
|
|
183
|
+
ordinal = excluded.ordinal,
|
|
184
|
+
top_level = excluded.top_level,
|
|
185
|
+
pending = excluded.pending,
|
|
186
|
+
assistant_id = excluded.assistant_id,
|
|
187
|
+
updated_seq = excluded.updated_seq,
|
|
188
|
+
envelope_json = excluded.envelope_json`, slot, ...row);
|
|
189
|
+
for (const alias of plan.aliases) sql.exec(`INSERT INTO _eda_transcript_aliases (slot, namespace, key, record_id) VALUES (?, ?, ?, ?)
|
|
190
|
+
ON CONFLICT(slot, namespace, key) DO NOTHING`, slot, alias.namespace, alias.key, alias.recordId);
|
|
191
|
+
sql.exec("UPDATE _eda_transcript_meta SET cursor = ? WHERE slot = ?", plan.throughSeq, slot);
|
|
192
|
+
};
|
|
193
|
+
const readRecord = (sql, slot, id) => {
|
|
194
|
+
const row = sql.exec("SELECT envelope_json FROM _eda_transcript_records WHERE slot = ? AND id = ?", slot, id).toArray()[0];
|
|
195
|
+
return row === void 0 ? void 0 : decodeRecordRow(row);
|
|
196
|
+
};
|
|
197
|
+
const decodeRecordId = Schema.decodeUnknownSync(TranscriptRecordId);
|
|
198
|
+
const readAlias = (sql, slot, namespace, key) => {
|
|
199
|
+
const row = sql.exec("SELECT record_id FROM _eda_transcript_aliases WHERE slot = ? AND namespace = ? AND key = ?", slot, namespace, key).toArray()[0];
|
|
200
|
+
return row === void 0 ? void 0 : decodeRecordId(row.record_id);
|
|
201
|
+
};
|
|
202
|
+
/** Indexed synchronous reads of one slot as committed so far in this transaction. */
|
|
203
|
+
const makeTranscriptLookup = (sql, slot) => ({
|
|
204
|
+
cursor: slot.cursor,
|
|
205
|
+
record: (id) => readRecord(sql, slot.slot, id),
|
|
206
|
+
alias: (namespace, key) => readAlias(sql, slot.slot, namespace, key),
|
|
207
|
+
nextOrdinal: (anchorSeq) => sql.exec(`SELECT MAX(ordinal) + 1 AS next FROM _eda_transcript_records
|
|
208
|
+
WHERE slot = ? AND anchor_seq = ?`, slot.slot, anchorSeq).one().next ?? 0
|
|
209
|
+
});
|
|
210
|
+
const topLevel = (record) => {
|
|
211
|
+
if (!isTopLevelTranscriptRecord(record)) throw new TranscriptRowDecodeError(`transcript record ${record.id} is indexed as top-level but is not`);
|
|
212
|
+
return record;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Window reads over one served slot. Every method is a synchronous indexed query, so a window
|
|
216
|
+
* assembled from them inside one synchronous call is one cut. Record ids are ASCII, so SQLite's
|
|
217
|
+
* binary text order equals `TranscriptOrder`'s code-unit order.
|
|
218
|
+
*/
|
|
219
|
+
const makeTranscriptWindowSource = (sql, incarnation, slot, carried) => {
|
|
220
|
+
const meta = transcriptVersionMetaOf(slot);
|
|
221
|
+
if (meta === void 0) throw new Error(`retired transcript slot ${slot.slot} is not servable`);
|
|
222
|
+
const topLevelPage = (key, limit, inclusive, direction) => {
|
|
223
|
+
const comparison = direction === "before" ? inclusive ? "<=" : "<" : inclusive ? ">=" : ">";
|
|
224
|
+
const order = direction === "before" ? "DESC" : "ASC";
|
|
225
|
+
const keyClause = key === void 0 ? "" : `AND (anchor_seq, ordinal, id) ${comparison} (?, ?, ?)`;
|
|
226
|
+
const keyBindings = key === void 0 ? [] : [
|
|
227
|
+
key.anchorSeq,
|
|
228
|
+
key.ordinal,
|
|
229
|
+
key.recordId
|
|
230
|
+
];
|
|
231
|
+
return sql.exec(`SELECT envelope_json FROM _eda_transcript_records
|
|
232
|
+
WHERE slot = ? AND top_level = 1 ${keyClause}
|
|
233
|
+
ORDER BY anchor_seq ${order}, ordinal ${order}, id ${order}
|
|
234
|
+
LIMIT ?`, slot.slot, ...keyBindings, limit).toArray().map((row) => topLevel(decodeRecordRow(row)));
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
incarnation,
|
|
238
|
+
meta,
|
|
239
|
+
carried,
|
|
240
|
+
topLevelBefore: (key, limit, inclusive) => topLevelPage(key, limit, inclusive, "before"),
|
|
241
|
+
topLevelAfter: (key, limit, inclusive) => topLevelPage(key, limit, inclusive, "after"),
|
|
242
|
+
record: (id) => readRecord(sql, slot.slot, id),
|
|
243
|
+
alias: (namespace, key) => readAlias(sql, slot.slot, namespace, key),
|
|
244
|
+
toolsOf: (assistantId) => sql.exec("SELECT envelope_json FROM _eda_transcript_records WHERE slot = ? AND assistant_id = ?", slot.slot, assistantId).toArray().flatMap((row) => {
|
|
245
|
+
const record = decodeRecordRow(row);
|
|
246
|
+
return record.kind === "tool" ? [record] : [];
|
|
247
|
+
}),
|
|
248
|
+
pendingUsers: () => sql.exec("SELECT envelope_json FROM _eda_transcript_records WHERE slot = ? AND pending = 1", slot.slot).toArray().flatMap((row) => {
|
|
249
|
+
const record = decodeRecordRow(row);
|
|
250
|
+
return record.kind === "user" ? [record] : [];
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
const mintUuidV7 = () => Effect.runSync(Effect.gen(function* () {
|
|
255
|
+
return yield* (yield* IdGenerator).makeCorrelationId();
|
|
256
|
+
}).pipe(Effect.provide(IdGenerator.Live)));
|
|
257
|
+
/**
|
|
258
|
+
* Read this log's incarnation, minting one on first need. The caller runs it inside a
|
|
259
|
+
* `transactionSync`, so read-or-mint is atomic; storage wipes delete the row and so mint anew.
|
|
260
|
+
*/
|
|
261
|
+
const readOrMintLogIncarnation = (sql, adopt) => {
|
|
262
|
+
const row = sql.exec("SELECT incarnation FROM _eda_log_incarnation WHERE id = 1").toArray()[0];
|
|
263
|
+
if (row !== void 0) return TranscriptLogIncarnation.make(row.incarnation);
|
|
264
|
+
const adopted = adopt?.kv.get(adopt.key);
|
|
265
|
+
const incarnation = typeof adopted === "string" && adopted.length > 0 ? adopted : mintUuidV7();
|
|
266
|
+
if (adopt !== void 0 && adopted !== incarnation) adopt.kv.put(adopt.key, incarnation);
|
|
267
|
+
sql.exec("INSERT INTO _eda_log_incarnation (id, incarnation) VALUES (1, ?)", incarnation);
|
|
268
|
+
return TranscriptLogIncarnation.make(incarnation);
|
|
269
|
+
};
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/durable-object-transcript-maintenance.ts
|
|
272
|
+
const defaultTranscriptSliceLimits = {
|
|
273
|
+
maxEvents: 512,
|
|
274
|
+
pageEvents: 16,
|
|
275
|
+
maxDeletedRows: 2e3
|
|
276
|
+
};
|
|
277
|
+
const emptyTranscriptEffects = () => ({
|
|
278
|
+
alerts: [],
|
|
279
|
+
pendingWork: false
|
|
280
|
+
});
|
|
281
|
+
/**
|
|
282
|
+
* Publish committed outcomes to the host. Call only after the outer transaction returns. A hook
|
|
283
|
+
* that throws is reported and otherwise ignored: the work it describes is already committed.
|
|
284
|
+
*/
|
|
285
|
+
const publishTranscriptEffects = (observer, effects) => {
|
|
286
|
+
const notify = (hook) => {
|
|
287
|
+
try {
|
|
288
|
+
hook();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("EDA transcript observer failed", error);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
for (const alert of effects.alerts) notify(() => observer?.onAlert?.(alert));
|
|
294
|
+
const activated = effects.activated;
|
|
295
|
+
if (activated !== void 0) notify(() => observer?.onActivated?.(activated));
|
|
296
|
+
if (effects.pendingWork) notify(() => observer?.onPendingWork?.());
|
|
297
|
+
};
|
|
298
|
+
/** The carried definition for a stored version, if this release still carries it. */
|
|
299
|
+
const carriedDefinition = (config, version) => config.current.version === version ? config.current : config.previous?.version === version ? config.previous : void 0;
|
|
300
|
+
const liveSlots = (storage) => readTranscriptSlots(storage.sql).filter((slot) => slot.role !== "retired");
|
|
301
|
+
/**
|
|
302
|
+
* Bring the stored versions in line with this release's definitions (§7 steps 2, 3 and 5),
|
|
303
|
+
* writing only metadata. Rows are never touched here: building and cleanup happen in slices.
|
|
304
|
+
*
|
|
305
|
+
* - No version yet: an empty log starts `current` as active at seq 0; a log written before
|
|
306
|
+
* transcripts existed builds `current` from the log while nothing is served.
|
|
307
|
+
* - A building version that is not `current` is retired (discarded).
|
|
308
|
+
* - If the active version is not `current` and no build of `current` exists, one starts. A build
|
|
309
|
+
* of `current` that failed counts as existing, so it is never retried.
|
|
310
|
+
*/
|
|
311
|
+
const reconcileTranscriptSlots = (storage, config, head, effects) => {
|
|
312
|
+
const { sql } = storage;
|
|
313
|
+
const current = config.current.version;
|
|
314
|
+
const slots = liveSlots(storage);
|
|
315
|
+
if (slots.length === 0) {
|
|
316
|
+
insertTranscriptSlot(sql, current, head === 0 ? "active" : "building");
|
|
317
|
+
if (head > 0) effects.pendingWork = true;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const slot of slots) if (slot.role === "building" && slot.version !== current) {
|
|
321
|
+
setTranscriptSlotRole(sql, slot.slot, "retired");
|
|
322
|
+
effects.pendingWork = true;
|
|
323
|
+
}
|
|
324
|
+
const active = slots.find((slot) => slot.role === "active");
|
|
325
|
+
const buildingCurrent = slots.some((slot) => slot.role === "building" && slot.version === current);
|
|
326
|
+
if (active?.version !== current && !buildingCurrent) {
|
|
327
|
+
insertTranscriptSlot(sql, current, "building");
|
|
328
|
+
effects.pendingWork = true;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const nestedTransactionSupport = /* @__PURE__ */ new WeakMap();
|
|
332
|
+
const nestedTransactionRejection = /cannot start a transaction within a transaction/i;
|
|
333
|
+
/**
|
|
334
|
+
* Whether this storage runs a `transactionSync` nested in another as an independent inner
|
|
335
|
+
* transaction. workerd does; celld rejects nesting ("cannot start a transaction within a
|
|
336
|
+
* transaction") without disturbing the enclosing transaction. Probed once per storage object.
|
|
337
|
+
*/
|
|
338
|
+
const supportsNestedTransactions = (storage) => {
|
|
339
|
+
const known = nestedTransactionSupport.get(storage);
|
|
340
|
+
if (known !== void 0) return known;
|
|
341
|
+
let supported;
|
|
342
|
+
try {
|
|
343
|
+
storage.transactionSync(() => storage.transactionSync(() => void 0));
|
|
344
|
+
supported = true;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!(error instanceof Error && nestedTransactionRejection.test(error.message))) throw error;
|
|
347
|
+
supported = false;
|
|
348
|
+
}
|
|
349
|
+
nestedTransactionSupport.set(storage, supported);
|
|
350
|
+
return supported;
|
|
351
|
+
};
|
|
352
|
+
/** Freeze a slot at its unchanged cursor and raise the alert. */
|
|
353
|
+
const freezeSlot = (storage, slot, failedAtSeq, cause, effects) => {
|
|
354
|
+
const error = cause instanceof Error ? cause.message : String(cause);
|
|
355
|
+
markTranscriptSlotFailed(storage.sql, slot.slot, failedAtSeq, error);
|
|
356
|
+
if (slot.role !== "retired") effects.alerts.push({
|
|
357
|
+
_tag: "ProjectionFailed",
|
|
358
|
+
version: slot.version,
|
|
359
|
+
role: slot.role,
|
|
360
|
+
failedAtSeq,
|
|
361
|
+
error
|
|
362
|
+
});
|
|
363
|
+
return {
|
|
364
|
+
...slot,
|
|
365
|
+
status: {
|
|
366
|
+
_tag: "Failed",
|
|
367
|
+
failedAtSeq,
|
|
368
|
+
error
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Project committed events into one slot, or freeze the slot (§5.2).
|
|
374
|
+
*
|
|
375
|
+
* Planning, row decoding and row encoding (serialization and the size cap) all run before any
|
|
376
|
+
* write, so every recoverable projection error is raised before the slot is touched. Where the
|
|
377
|
+
* platform nests transactions (workerd), the writes also run inside an inner `transactionSync`,
|
|
378
|
+
* so even a failing write rolls back only that slot. Either way the failure is recorded in the
|
|
379
|
+
* caller's transaction and the events the caller appended still commit. Where it does not
|
|
380
|
+
* (celld), a write that fails after planning succeeded is a storage failure, which fails the
|
|
381
|
+
* caller's whole transaction atomically, exactly as a failed event insert does.
|
|
382
|
+
*/
|
|
383
|
+
const advanceSlot = (storage, slot, definition, events, effects) => {
|
|
384
|
+
const first = events.find((entry) => entry.position.seq > slot.cursor);
|
|
385
|
+
if (first === void 0 || slot.status._tag === "Failed" || slot.role === "retired") return slot;
|
|
386
|
+
const nested = supportsNestedTransactions(storage);
|
|
387
|
+
const project = () => {
|
|
388
|
+
const plan = encodeTranscriptPlan(planTranscriptBatch(makeTranscriptLookup(storage.sql, slot), events, definition));
|
|
389
|
+
if (nested) writeTranscriptPlan(storage.sql, slot.slot, plan);
|
|
390
|
+
return plan;
|
|
391
|
+
};
|
|
392
|
+
let plan;
|
|
393
|
+
try {
|
|
394
|
+
plan = nested ? storage.transactionSync(project) : project();
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
return freezeSlot(storage, slot, first.position.seq, cause, effects);
|
|
397
|
+
}
|
|
398
|
+
if (!nested) writeTranscriptPlan(storage.sql, slot.slot, plan);
|
|
399
|
+
return {
|
|
400
|
+
...slot,
|
|
401
|
+
cursor: plan.throughSeq
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* The append hook: one batch-level call inside the append transaction, after every entry has
|
|
406
|
+
* been inserted.
|
|
407
|
+
*
|
|
408
|
+
* A version is maintained here exactly when it is carried, healthy and caught up — its cursor is
|
|
409
|
+
* the head the batch started from — so the new events are precisely what it is missing. Anything
|
|
410
|
+
* behind (a building version) is caught up by builder slices instead, and frozen versions are
|
|
411
|
+
* never touched. Each version fails independently.
|
|
412
|
+
*/
|
|
413
|
+
const maintainTranscriptsOnAppend = (storage, config, headBeforeBatch, appended, effects) => {
|
|
414
|
+
reconcileTranscriptSlots(storage, config, headBeforeBatch, effects);
|
|
415
|
+
if (appended.length === 0) return;
|
|
416
|
+
for (const slot of liveSlots(storage)) {
|
|
417
|
+
const definition = carriedDefinition(config, slot.version);
|
|
418
|
+
if (definition === void 0 || slot.status._tag === "Failed") continue;
|
|
419
|
+
if (slot.cursor !== headBeforeBatch) continue;
|
|
420
|
+
advanceSlot(storage, slot, definition, appended, effects);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* The missed-session alert for a session whose active version this release no longer carries,
|
|
425
|
+
* raised whenever an object starts, so it does not depend on observing the build's creation.
|
|
426
|
+
*/
|
|
427
|
+
const missedSessionAlert = (storage, config) => {
|
|
428
|
+
const active = liveSlots(storage).find((slot) => slot.role === "active");
|
|
429
|
+
return active === void 0 || carriedDefinition(config, active.version) !== void 0 ? void 0 : {
|
|
430
|
+
_tag: "MissedSession",
|
|
431
|
+
activeVersion: active.version,
|
|
432
|
+
currentVersion: config.current.version
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
/** The one slot a builder slice advances: the building version, else an active one behind. */
|
|
436
|
+
const buildTarget = (config, slots, head) => {
|
|
437
|
+
const healthy = (slot) => slot.status._tag === "Ok" && carriedDefinition(config, slot.version) !== void 0;
|
|
438
|
+
return slots.find((slot) => slot.role === "building" && healthy(slot)) ?? slots.find((slot) => slot.role === "active" && healthy(slot) && slot.cursor < head);
|
|
439
|
+
};
|
|
440
|
+
/** Whether any builder or cleanup slice has work to do. Read-only. */
|
|
441
|
+
const hasPendingTranscriptWork = (storage, config, head) => {
|
|
442
|
+
const slots = readTranscriptSlots(storage.sql);
|
|
443
|
+
if (slots.some((slot) => slot.role === "retired")) return true;
|
|
444
|
+
const live = slots.filter((slot) => slot.role !== "retired");
|
|
445
|
+
if (live.length === 0) return head > 0;
|
|
446
|
+
const current = config.current.version;
|
|
447
|
+
if (live.some((slot) => slot.role === "building" && slot.version !== current)) return true;
|
|
448
|
+
if (live.find((slot) => slot.role === "active")?.version !== current && !live.some((slot) => slot.role === "building" && slot.version === current)) return true;
|
|
449
|
+
return buildTarget(config, live, head) !== void 0;
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* One bounded builder slice. The caller runs it inside one outer `transactionSync`.
|
|
453
|
+
*
|
|
454
|
+
* It reconciles versions, projects at most `maxEvents` log events into the building version
|
|
455
|
+
* (page by page, each page's rows and cursor committed together, so an eviction resumes from the
|
|
456
|
+
* cursor), activates the build when its cursor equals the head re-read in this transaction, and
|
|
457
|
+
* deletes a bounded number of retired rows. Returns whether more work remains.
|
|
458
|
+
*/
|
|
459
|
+
const runTranscriptSlice = (storage, config, log, limits, effects) => {
|
|
460
|
+
const { sql } = storage;
|
|
461
|
+
const head = log.head();
|
|
462
|
+
reconcileTranscriptSlots(storage, config, head, effects);
|
|
463
|
+
const slots = liveSlots(storage);
|
|
464
|
+
const target = buildTarget(config, slots, head);
|
|
465
|
+
if (target !== void 0) {
|
|
466
|
+
const definition = carriedDefinition(config, target.version);
|
|
467
|
+
let slot = target;
|
|
468
|
+
let remaining = limits.maxEvents;
|
|
469
|
+
while (definition !== void 0 && slot.status._tag === "Ok" && slot.cursor < head && remaining > 0) {
|
|
470
|
+
let page;
|
|
471
|
+
try {
|
|
472
|
+
page = log.after(slot.cursor, Math.min(limits.pageEvents, remaining));
|
|
473
|
+
} catch (cause) {
|
|
474
|
+
slot = freezeSlot(storage, slot, SequenceNumber.make(slot.cursor + 1), cause, effects);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (page.length === 0) break;
|
|
478
|
+
remaining -= page.length;
|
|
479
|
+
slot = advanceSlot(storage, slot, definition, page, effects);
|
|
480
|
+
}
|
|
481
|
+
if (slot.role === "building" && slot.status._tag === "Ok" && slot.cursor === log.head()) {
|
|
482
|
+
const previous = slots.find((candidate) => candidate.role === "active");
|
|
483
|
+
if (previous !== void 0) setTranscriptSlotRole(sql, previous.slot, "retired");
|
|
484
|
+
setTranscriptSlotRole(sql, slot.slot, "active");
|
|
485
|
+
effects.activated = {
|
|
486
|
+
version: slot.version,
|
|
487
|
+
previousVersion: previous?.version
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
let deleteBudget = limits.maxDeletedRows;
|
|
492
|
+
for (const slot of readTranscriptSlots(sql)) {
|
|
493
|
+
if (deleteBudget <= 0) break;
|
|
494
|
+
if (slot.role === "retired") deleteBudget -= deleteRetiredTranscriptRows(sql, slot.slot, deleteBudget);
|
|
495
|
+
}
|
|
496
|
+
return hasPendingTranscriptWork(storage, config, log.head());
|
|
497
|
+
};
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/durable-object-store.ts
|
|
500
|
+
const ProjectionPayload = Schema.Record(Schema.String, Schema.Unknown);
|
|
501
|
+
const AdmittedCommandProjection = Schema.Struct({
|
|
502
|
+
commandId: Schema.Unknown,
|
|
503
|
+
idempotencyKey: Schema.optionalKey(Schema.Unknown)
|
|
504
|
+
});
|
|
505
|
+
/** Squashed pre-release framework schema. */
|
|
506
|
+
const durableObjectSessionStoreSchemaVersion = 1;
|
|
507
|
+
/** Forward migration adding the transcript projection, log incarnation and alarm wakeups. */
|
|
508
|
+
const durableObjectTranscriptSchemaVersion = 2;
|
|
509
|
+
/**
|
|
510
|
+
* Bound one synchronous DO SQLite event-log read/decode to a small CPU and memory slice.
|
|
511
|
+
*
|
|
512
|
+
* A single logical event can already approach the per-row JSON hard cap, and logical decode may
|
|
513
|
+
* touch sidecar rows. Sixteen rows keeps worst-case page memory materially below isolate limits
|
|
514
|
+
* while still amortizing SQL cursor setup for normal small framework events. Revisit with page
|
|
515
|
+
* duration/byte telemetry before increasing.
|
|
516
|
+
*/
|
|
517
|
+
const durableObjectEventReadPageRows = 16;
|
|
518
|
+
/**
|
|
519
|
+
* Durable Object SQLite-backed implementation of the semantic `EDASessionStore`.
|
|
520
|
+
*
|
|
521
|
+
* This adapter is the only place EDA framework storage code touches DO SQL. It
|
|
522
|
+
* translates domain append intent into synchronous SQLite writes while keeping
|
|
523
|
+
* `SessionState` and runners SQL-agnostic.
|
|
524
|
+
*/
|
|
525
|
+
var DurableObjectSessionStore = class DurableObjectSessionStore {
|
|
526
|
+
/** Run the single squashed framework migration for the current pre-release schema. */
|
|
527
|
+
static migrate = (storage) => migrateDurableObjectSessionStore(storage);
|
|
528
|
+
/** Build a session-scoped store after migrations have completed. */
|
|
529
|
+
static make = ({ sessionId, storage, transcript }) => Effect.gen(function* () {
|
|
530
|
+
yield* DurableObjectSessionStore.migrate(storage);
|
|
531
|
+
return makeDurableObjectSessionStore(sessionId, storage, transcript);
|
|
532
|
+
});
|
|
533
|
+
/** Layer constructor used by the Durable Object host runtime graph. */
|
|
534
|
+
static layer = (options) => Layer.effect(EDASessionStore, DurableObjectSessionStore.make(options));
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* Create all framework-owned tables in one idempotent startup step.
|
|
538
|
+
*
|
|
539
|
+
* The project is still pre-release, so this remains a squashed version-1 schema
|
|
540
|
+
* rather than a chain of compatibility migrations.
|
|
541
|
+
*/
|
|
542
|
+
const migrateDurableObjectSessionStore = (storage) => Effect.try({
|
|
543
|
+
try: () => storage.transactionSync(() => {
|
|
544
|
+
const { sql } = storage;
|
|
545
|
+
sql.exec(`
|
|
546
|
+
CREATE TABLE IF NOT EXISTS _eda_schema_migrations (
|
|
547
|
+
id INTEGER PRIMARY KEY,
|
|
548
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
549
|
+
)
|
|
550
|
+
`);
|
|
551
|
+
const currentVersion = sql.exec("SELECT COALESCE(MAX(id), 0) AS version FROM _eda_schema_migrations").one().version ?? 0;
|
|
552
|
+
if (currentVersion < durableObjectSessionStoreSchemaVersion) {
|
|
553
|
+
sql.exec(`
|
|
554
|
+
CREATE TABLE IF NOT EXISTS _eda_event_log (
|
|
555
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
556
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
557
|
+
namespace TEXT NOT NULL,
|
|
558
|
+
type TEXT NOT NULL,
|
|
559
|
+
schema_version INTEGER NOT NULL,
|
|
560
|
+
created_at_ms INTEGER NOT NULL,
|
|
561
|
+
trace_json TEXT NOT NULL,
|
|
562
|
+
fact_json TEXT NOT NULL
|
|
563
|
+
)
|
|
564
|
+
`);
|
|
565
|
+
sql.exec(`
|
|
566
|
+
CREATE INDEX IF NOT EXISTS _eda_event_log_namespace_type_seq_idx
|
|
567
|
+
ON _eda_event_log(namespace, type, seq)
|
|
568
|
+
`);
|
|
569
|
+
sql.exec(`
|
|
570
|
+
CREATE TABLE IF NOT EXISTS _eda_command_state (
|
|
571
|
+
command_id TEXT PRIMARY KEY,
|
|
572
|
+
admitted_seq INTEGER NOT NULL,
|
|
573
|
+
status TEXT NOT NULL,
|
|
574
|
+
idempotency_key TEXT,
|
|
575
|
+
payload_json TEXT NOT NULL
|
|
576
|
+
)
|
|
577
|
+
`);
|
|
578
|
+
sql.exec(`
|
|
579
|
+
CREATE TABLE IF NOT EXISTS _eda_command_inputs (
|
|
580
|
+
command_id TEXT PRIMARY KEY,
|
|
581
|
+
admitted_seq INTEGER NOT NULL UNIQUE,
|
|
582
|
+
payload_json TEXT NOT NULL
|
|
583
|
+
)
|
|
584
|
+
`);
|
|
585
|
+
sql.exec(`
|
|
586
|
+
CREATE INDEX IF NOT EXISTS _eda_command_state_status_seq_idx
|
|
587
|
+
ON _eda_command_state(status, admitted_seq)
|
|
588
|
+
`);
|
|
589
|
+
sql.exec(`
|
|
590
|
+
CREATE UNIQUE INDEX IF NOT EXISTS _eda_command_state_idempotency_idx
|
|
591
|
+
ON _eda_command_state(idempotency_key) WHERE idempotency_key IS NOT NULL
|
|
592
|
+
`);
|
|
593
|
+
sql.exec(`
|
|
594
|
+
CREATE TABLE IF NOT EXISTS _eda_context_messages (
|
|
595
|
+
message_id TEXT PRIMARY KEY,
|
|
596
|
+
context_seq INTEGER NOT NULL UNIQUE,
|
|
597
|
+
payload_json TEXT NOT NULL
|
|
598
|
+
)
|
|
599
|
+
`);
|
|
600
|
+
sql.exec(`
|
|
601
|
+
CREATE INDEX IF NOT EXISTS _eda_context_messages_context_seq_idx
|
|
602
|
+
ON _eda_context_messages(context_seq)
|
|
603
|
+
`);
|
|
604
|
+
sql.exec(`
|
|
605
|
+
CREATE TABLE IF NOT EXISTS _eda_context_summaries (
|
|
606
|
+
summary_id TEXT PRIMARY KEY,
|
|
607
|
+
created_seq INTEGER NOT NULL UNIQUE,
|
|
608
|
+
payload_json TEXT NOT NULL
|
|
609
|
+
)
|
|
610
|
+
`);
|
|
611
|
+
sql.exec(`
|
|
612
|
+
CREATE INDEX IF NOT EXISTS _eda_context_summaries_created_seq_idx
|
|
613
|
+
ON _eda_context_summaries(created_seq)
|
|
614
|
+
`);
|
|
615
|
+
sql.exec(`
|
|
616
|
+
CREATE TABLE IF NOT EXISTS _eda_reducer_checkpoints (
|
|
617
|
+
reducer_name TEXT PRIMARY KEY,
|
|
618
|
+
schema_version INTEGER NOT NULL,
|
|
619
|
+
through_seq INTEGER NOT NULL,
|
|
620
|
+
payload_json TEXT NOT NULL,
|
|
621
|
+
updated_at_ms INTEGER NOT NULL
|
|
622
|
+
)
|
|
623
|
+
`);
|
|
624
|
+
sql.exec(`
|
|
625
|
+
CREATE TABLE IF NOT EXISTS _eda_sink_cursors (
|
|
626
|
+
sink_name TEXT PRIMARY KEY,
|
|
627
|
+
after_seq INTEGER NOT NULL,
|
|
628
|
+
payload_json TEXT NOT NULL
|
|
629
|
+
)
|
|
630
|
+
`);
|
|
631
|
+
sql.exec("INSERT INTO _eda_schema_migrations (id) VALUES (?)", durableObjectSessionStoreSchemaVersion);
|
|
632
|
+
}
|
|
633
|
+
if (currentVersion < durableObjectTranscriptSchemaVersion) {
|
|
634
|
+
for (const statement of transcriptTableStatements) sql.exec(statement);
|
|
635
|
+
sql.exec("INSERT INTO _eda_schema_migrations (id) VALUES (?)", durableObjectTranscriptSchemaVersion);
|
|
636
|
+
}
|
|
637
|
+
}),
|
|
638
|
+
catch: (error) => durableStoreError("migrating DurableObjectSessionStore", error)
|
|
639
|
+
});
|
|
640
|
+
/**
|
|
641
|
+
* Interpret the semantic store API with native DO SQLite.
|
|
642
|
+
*
|
|
643
|
+
* All writes that affect durable ordering happen inside `transactionSync`; replay
|
|
644
|
+
* and lookup reads stay synchronous SQL wrapped in Effect boundaries.
|
|
645
|
+
*/
|
|
646
|
+
const makeDurableObjectSessionStore = (sessionId, storage, transcript) => {
|
|
647
|
+
const { sql } = storage;
|
|
648
|
+
const ensureSession = (event) => {
|
|
649
|
+
if (event.sessionId !== sessionId) throw new Error(`DurableObjectSessionStore is scoped to session ${sessionId}; received ${event.sessionId}`);
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* Insert one append entry or return its existing idempotent event position.
|
|
653
|
+
*
|
|
654
|
+
* The event row is the sequence allocator. Artifact/projection writes happen
|
|
655
|
+
* only for newly inserted events, preventing duplicate appends from advancing
|
|
656
|
+
* body rows or metadata; only those are collected into `inserted`.
|
|
657
|
+
*/
|
|
658
|
+
const insertOrRead = (entry, inserted) => {
|
|
659
|
+
const { event } = entry;
|
|
660
|
+
ensureSession(event);
|
|
661
|
+
const existing = readCommittedByEventIdOptional(sql, sessionId, event.eventId);
|
|
662
|
+
if (existing !== void 0) return existing;
|
|
663
|
+
const encoded = encodeEventLogRow(event);
|
|
664
|
+
sql.exec(`
|
|
665
|
+
INSERT INTO _eda_event_log (
|
|
666
|
+
event_id,
|
|
667
|
+
namespace,
|
|
668
|
+
type,
|
|
669
|
+
schema_version,
|
|
670
|
+
created_at_ms,
|
|
671
|
+
trace_json,
|
|
672
|
+
fact_json
|
|
673
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
674
|
+
`, encoded.eventId, encoded.namespace, encoded.type, encoded.schemaVersion, encoded.createdAtMs, encoded.traceJson, encoded.factJson);
|
|
675
|
+
const row = readEventLogRowByEventId(sql, event.eventId);
|
|
676
|
+
applySynchronousProjections(sql, entry, SequenceNumber.make(row.seq), encoded.factJson);
|
|
677
|
+
const committed = rowToCommittedDurableEvent(sql, row, sessionId);
|
|
678
|
+
inserted.push(committed);
|
|
679
|
+
return committed;
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* Execute an entire append batch as one synchronous SQLite transaction.
|
|
683
|
+
*
|
|
684
|
+
* Transcript maintenance is one batch-level hook inside the same transaction, after every
|
|
685
|
+
* entry. A projection failure freezes only the failing version and never fails the append;
|
|
686
|
+
* its alert is published only after the transaction commits.
|
|
687
|
+
*/
|
|
688
|
+
const append = (batch) => Effect.try({
|
|
689
|
+
try: () => {
|
|
690
|
+
const effects = emptyTranscriptEffects();
|
|
691
|
+
const committed = storage.transactionSync(() => {
|
|
692
|
+
const headBeforeBatch = transcript === void 0 ? void 0 : readEventLogHead(sql);
|
|
693
|
+
const inserted = [];
|
|
694
|
+
const entries = batch.entries.map((entry) => insertOrRead(entry, inserted));
|
|
695
|
+
if (transcript !== void 0 && headBeforeBatch !== void 0) maintainTranscriptsOnAppend(storage, transcript.definitions, headBeforeBatch, inserted, effects);
|
|
696
|
+
return entries;
|
|
697
|
+
});
|
|
698
|
+
publishTranscriptEffects(transcript?.observer, effects);
|
|
699
|
+
return committed;
|
|
700
|
+
},
|
|
701
|
+
catch: (error) => durableStoreError("appending DurableObjectSessionStore batch", error)
|
|
702
|
+
}).pipe(Effect.tap((committed) => annotateEdaSpan(committedBatchAttributes(committed))));
|
|
703
|
+
return {
|
|
704
|
+
append,
|
|
705
|
+
eventsAfter: (afterSeq) => makeCommittedEventStream(sql, sessionId, afterSeq),
|
|
706
|
+
loadCommittedEventsBySeq: (seqs) => Effect.try({
|
|
707
|
+
try: () => uniqueSequenceNumbers(seqs).sort((left, right) => left - right).map((seq) => readCommittedBySeq(sql, sessionId, seq)),
|
|
708
|
+
catch: (error) => durableStoreError("loading DurableObjectSessionStore events by seq", error)
|
|
709
|
+
}),
|
|
710
|
+
findCommandAdmission: (input) => Effect.try({
|
|
711
|
+
try: () => {
|
|
712
|
+
const row = findCommandAdmissionRow(sql, input.commandId, input.idempotencyKey);
|
|
713
|
+
return row === void 0 ? void 0 : readCommittedBySeq(sql, sessionId, row.admitted_seq);
|
|
714
|
+
},
|
|
715
|
+
catch: (error) => durableStoreError("finding command admission", error)
|
|
716
|
+
}),
|
|
717
|
+
loadSummaryArtifact: (summaryId) => Effect.try({
|
|
718
|
+
try: () => readSummaryPayloadOptional(sql, summaryId),
|
|
719
|
+
catch: (error) => durableStoreError("loading DurableObjectSessionStore summary", error)
|
|
720
|
+
}),
|
|
721
|
+
loadReducerCheckpoint: (name) => Effect.try({
|
|
722
|
+
try: () => readReducerCheckpoint(sql, name),
|
|
723
|
+
catch: (error) => durableStoreError(`loading DurableObjectSessionStore reducer checkpoint ${name}`, error)
|
|
724
|
+
}),
|
|
725
|
+
saveReducerCheckpoint: (checkpoint) => Effect.try({
|
|
726
|
+
try: () => writeReducerCheckpoint(sql, checkpoint),
|
|
727
|
+
catch: (error) => durableStoreError(`saving DurableObjectSessionStore reducer checkpoint ${checkpoint.name}`, error)
|
|
728
|
+
}),
|
|
729
|
+
saveReducerCheckpoints: (checkpoints) => Effect.try({
|
|
730
|
+
try: () => storage.transactionSync(() => checkpoints.forEach((checkpoint) => writeReducerCheckpoint(sql, checkpoint))),
|
|
731
|
+
catch: (error) => durableStoreError("saving DurableObjectSessionStore reducer checkpoints", error)
|
|
732
|
+
})
|
|
733
|
+
};
|
|
734
|
+
};
|
|
735
|
+
/** Synchronous committed-log reads for the transcript builder, decoded like every replay. */
|
|
736
|
+
const makeDurableObjectEventLog = (sql, sessionId) => ({
|
|
737
|
+
head: () => readEventLogHead(sql),
|
|
738
|
+
after: (afterSeq, limit) => sql.exec(`
|
|
739
|
+
SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
|
|
740
|
+
FROM _eda_event_log
|
|
741
|
+
WHERE seq > ?
|
|
742
|
+
ORDER BY seq ASC
|
|
743
|
+
LIMIT ?
|
|
744
|
+
`, afterSeq, limit).toArray().map((row) => rowToCommittedDurableEvent(sql, row, sessionId))
|
|
745
|
+
});
|
|
746
|
+
const makeCommittedEventStream = (sql, sessionId, afterSeq) => Stream.fromPull(Effect.sync(() => {
|
|
747
|
+
let cursor = {
|
|
748
|
+
afterSeq,
|
|
749
|
+
throughSeq: void 0
|
|
750
|
+
};
|
|
751
|
+
let done = false;
|
|
752
|
+
return Effect.gen(function* () {
|
|
753
|
+
if (done) return yield* Cause.done();
|
|
754
|
+
yield* Effect.yieldNow;
|
|
755
|
+
const page = yield* readCommittedEventPage(sql, sessionId, cursor);
|
|
756
|
+
if (page.events.length === 0) {
|
|
757
|
+
done = true;
|
|
758
|
+
return yield* Cause.done();
|
|
759
|
+
}
|
|
760
|
+
if (Option.isSome(page.next)) cursor = page.next.value;
|
|
761
|
+
else done = true;
|
|
762
|
+
return page.events;
|
|
763
|
+
});
|
|
764
|
+
}));
|
|
765
|
+
const readCommittedEventPage = (sql, sessionId, cursor) => Effect.try({
|
|
766
|
+
try: () => {
|
|
767
|
+
const throughSeq = cursor.throughSeq ?? readEventLogHead(sql);
|
|
768
|
+
if (cursor.afterSeq >= throughSeq) return {
|
|
769
|
+
events: [],
|
|
770
|
+
next: Option.none()
|
|
771
|
+
};
|
|
772
|
+
const events = sql.exec(`
|
|
773
|
+
SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
|
|
774
|
+
FROM _eda_event_log
|
|
775
|
+
WHERE seq > ? AND seq <= ?
|
|
776
|
+
ORDER BY seq ASC
|
|
777
|
+
LIMIT ?
|
|
778
|
+
`, cursor.afterSeq, throughSeq, durableObjectEventReadPageRows).toArray().map((row) => rowToCommittedDurableEvent(sql, row, sessionId));
|
|
779
|
+
const lastSeq = events.at(-1)?.position.seq ?? cursor.afterSeq;
|
|
780
|
+
return {
|
|
781
|
+
events,
|
|
782
|
+
next: lastSeq < throughSeq ? Option.some({
|
|
783
|
+
afterSeq: lastSeq,
|
|
784
|
+
throughSeq
|
|
785
|
+
}) : Option.none()
|
|
786
|
+
};
|
|
787
|
+
},
|
|
788
|
+
catch: (error) => durableStoreError("replaying DurableObjectSessionStore events", error)
|
|
789
|
+
});
|
|
790
|
+
const readEventLogHead = (sql) => SequenceNumber.make(sql.exec(`
|
|
791
|
+
SELECT COALESCE(MAX(seq), 0) AS head
|
|
792
|
+
FROM _eda_event_log
|
|
793
|
+
`).one().head ?? 0);
|
|
794
|
+
const uniqueSequenceNumbers = (seqs) => Array.from(new Set(seqs.map((seq) => Number(seq)))).map((seq) => SequenceNumber.make(seq));
|
|
795
|
+
const readEventLogRowByEventId = (sql, eventId) => sql.exec(`
|
|
796
|
+
SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
|
|
797
|
+
FROM _eda_event_log
|
|
798
|
+
WHERE event_id = ?
|
|
799
|
+
`, eventId).one();
|
|
800
|
+
const readCommittedByEventIdOptional = (sql, sessionId, eventId) => {
|
|
801
|
+
const row = sql.exec(`
|
|
802
|
+
SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
|
|
803
|
+
FROM _eda_event_log
|
|
804
|
+
WHERE event_id = ?
|
|
805
|
+
`, eventId).toArray()[0];
|
|
806
|
+
return row === void 0 ? void 0 : rowToCommittedDurableEvent(sql, row, sessionId);
|
|
807
|
+
};
|
|
808
|
+
const readReducerCheckpoint = (sql, name) => {
|
|
809
|
+
const row = sql.exec(`
|
|
810
|
+
SELECT reducer_name, schema_version, through_seq, payload_json, updated_at_ms
|
|
811
|
+
FROM _eda_reducer_checkpoints
|
|
812
|
+
WHERE reducer_name = ?
|
|
813
|
+
`, name).toArray()[0];
|
|
814
|
+
return row === void 0 ? void 0 : {
|
|
815
|
+
name: row.reducer_name,
|
|
816
|
+
schemaVersion: row.schema_version,
|
|
817
|
+
throughSeq: SequenceNumber.make(row.through_seq),
|
|
818
|
+
payload: JSON.parse(row.payload_json),
|
|
819
|
+
updatedAtMs: row.updated_at_ms
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
const writeReducerCheckpoint = (sql, checkpoint) => {
|
|
823
|
+
sql.exec(`
|
|
824
|
+
INSERT INTO _eda_reducer_checkpoints (
|
|
825
|
+
reducer_name,
|
|
826
|
+
schema_version,
|
|
827
|
+
through_seq,
|
|
828
|
+
payload_json,
|
|
829
|
+
updated_at_ms
|
|
830
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
831
|
+
ON CONFLICT(reducer_name) DO UPDATE SET
|
|
832
|
+
schema_version = excluded.schema_version,
|
|
833
|
+
through_seq = excluded.through_seq,
|
|
834
|
+
payload_json = excluded.payload_json,
|
|
835
|
+
updated_at_ms = excluded.updated_at_ms
|
|
836
|
+
`, checkpoint.name, checkpoint.schemaVersion, checkpoint.throughSeq, stringifyJsonForColumn(checkpoint.payload, "_eda_reducer_checkpoints.payload_json"), checkpoint.updatedAtMs);
|
|
837
|
+
};
|
|
838
|
+
const readMessagePayloadByContextSeq = (sql, contextSeq) => {
|
|
839
|
+
const message = sql.exec(`
|
|
840
|
+
SELECT context_seq, payload_json
|
|
841
|
+
FROM _eda_context_messages
|
|
842
|
+
WHERE context_seq = ?
|
|
843
|
+
`, contextSeq).toArray()[0];
|
|
844
|
+
if (message === void 0) throw new Error(`Context message at seq ${contextSeq} missing from _eda_context_messages`);
|
|
845
|
+
return message;
|
|
846
|
+
};
|
|
847
|
+
const readCommittedBySeq = (sql, sessionId, seq) => {
|
|
848
|
+
const row = sql.exec(`
|
|
849
|
+
SELECT seq, event_id, namespace, type, schema_version, created_at_ms, trace_json, fact_json
|
|
850
|
+
FROM _eda_event_log
|
|
851
|
+
WHERE seq = ?
|
|
852
|
+
`, seq).one();
|
|
853
|
+
return rowToCommittedDurableEvent(sql, row, sessionId);
|
|
854
|
+
};
|
|
855
|
+
const rowToCommittedDurableEvent = (sql, row, sessionId) => ({
|
|
856
|
+
position: durablePosition(SequenceNumber.make(row.seq)),
|
|
857
|
+
event: decodeDurableEventSync({
|
|
858
|
+
namespace: row.namespace,
|
|
859
|
+
type: row.type,
|
|
860
|
+
schemaVersion: row.schema_version,
|
|
861
|
+
durability: "durable",
|
|
862
|
+
eventId: row.event_id,
|
|
863
|
+
sessionId,
|
|
864
|
+
createdAtMs: row.created_at_ms,
|
|
865
|
+
trace: JSON.parse(row.trace_json),
|
|
866
|
+
payload: readLogicalPayload(sql, row)
|
|
867
|
+
})
|
|
868
|
+
});
|
|
869
|
+
const encodeEventLogRow = (event) => {
|
|
870
|
+
const encoded = Schema.encodeSync(DurableEventEnvelope)(event);
|
|
871
|
+
return {
|
|
872
|
+
eventId: encoded.eventId,
|
|
873
|
+
namespace: encoded.namespace,
|
|
874
|
+
type: encoded.type,
|
|
875
|
+
schemaVersion: encoded.schemaVersion,
|
|
876
|
+
createdAtMs: encoded.createdAtMs,
|
|
877
|
+
traceJson: stringifyJsonForColumn(encoded.trace, "_eda_event_log.trace_json"),
|
|
878
|
+
factJson: stringifyJsonForColumn(eventLogFactPayload(encoded.namespace, encoded.type, encoded.payload), "_eda_event_log.fact_json")
|
|
879
|
+
};
|
|
880
|
+
};
|
|
881
|
+
const readLogicalPayload = (sql, row) => {
|
|
882
|
+
if (row.namespace !== effectDurableAgentNamespace) return JSON.parse(row.fact_json);
|
|
883
|
+
if (row.type === commandAdmittedEventType) {
|
|
884
|
+
const commandId = readCommandIdFromFact(row);
|
|
885
|
+
return { command: readCommandInputPayload(sql, commandId) };
|
|
886
|
+
}
|
|
887
|
+
if (contextMessageEventTypes.has(row.type)) return JSON.parse(readMessagePayloadByContextSeq(sql, row.seq).payload_json);
|
|
888
|
+
if (row.type === summaryCreatedEventType) return {
|
|
889
|
+
...JSON.parse(row.fact_json),
|
|
890
|
+
summary: readSummaryPayloadFromCreatedSeq(sql, row.seq)
|
|
891
|
+
};
|
|
892
|
+
return JSON.parse(row.fact_json);
|
|
893
|
+
};
|
|
894
|
+
const eventLogFactPayload = (namespace, type, payload) => {
|
|
895
|
+
if (namespace !== effectDurableAgentNamespace || typeof payload !== "object" || payload === null) return payload;
|
|
896
|
+
const record = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))(payload);
|
|
897
|
+
switch (type) {
|
|
898
|
+
case commandAdmittedEventType: return { command: commandFact(record.command) };
|
|
899
|
+
case systemMessageCommittedEventType: return pick(record, ["messageId"]);
|
|
900
|
+
case userMessageCommittedEventType: return pick(record, ["commandId", "messageId"]);
|
|
901
|
+
case steeringMessageQueuedEventType: return pick(record, [
|
|
902
|
+
"commandId",
|
|
903
|
+
"messageId",
|
|
904
|
+
"runId"
|
|
905
|
+
]);
|
|
906
|
+
case assistantMessageCommittedEventType:
|
|
907
|
+
case assistantMessageImportedEventType: return pick(record, [
|
|
908
|
+
"messageId",
|
|
909
|
+
"runId",
|
|
910
|
+
"turnId",
|
|
911
|
+
"inferenceId"
|
|
912
|
+
]);
|
|
913
|
+
case assistantPartialCommittedEventType: return pick(record, [
|
|
914
|
+
"messageId",
|
|
915
|
+
"runId",
|
|
916
|
+
"turnId",
|
|
917
|
+
"inferenceId",
|
|
918
|
+
"reason"
|
|
919
|
+
]);
|
|
920
|
+
case summaryCreatedEventType: return pick(record, [
|
|
921
|
+
"compactionId",
|
|
922
|
+
"summaryId",
|
|
923
|
+
"sourceFromSeq",
|
|
924
|
+
"sourceToSeq"
|
|
925
|
+
]);
|
|
926
|
+
default: return payload;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const commandFact = (command) => {
|
|
930
|
+
if (typeof command !== "object" || command === null) return command;
|
|
931
|
+
const record = command;
|
|
932
|
+
return record._tag === "SubmitMessage" ? pick(record, [
|
|
933
|
+
"_tag",
|
|
934
|
+
"commandId",
|
|
935
|
+
"disposition",
|
|
936
|
+
"idempotencyKey"
|
|
937
|
+
]) : command;
|
|
938
|
+
};
|
|
939
|
+
const pick = (record, keys) => {
|
|
940
|
+
const out = {};
|
|
941
|
+
for (const key of keys) if (record[key] !== void 0) out[key] = record[key];
|
|
942
|
+
return out;
|
|
943
|
+
};
|
|
944
|
+
const readCommandIdFromFact = (row) => {
|
|
945
|
+
const { command: { commandId } } = Schema.decodeUnknownSync(Schema.Struct({ command: Schema.Struct({ commandId: Schema.String }) }))(JSON.parse(row.fact_json));
|
|
946
|
+
if (typeof commandId !== "string") throw new Error(`CommandAdmitted fact at seq ${row.seq} missing command.commandId`);
|
|
947
|
+
return commandId;
|
|
948
|
+
};
|
|
949
|
+
const readCommandInputPayload = (sql, commandId) => {
|
|
950
|
+
const row = sql.exec(`
|
|
951
|
+
SELECT payload_json
|
|
952
|
+
FROM _eda_command_inputs
|
|
953
|
+
WHERE command_id = ?
|
|
954
|
+
`, commandId).toArray()[0];
|
|
955
|
+
if (row === void 0) throw new Error(`Command input ${commandId} missing from _eda_command_inputs`);
|
|
956
|
+
return JSON.parse(row.payload_json);
|
|
957
|
+
};
|
|
958
|
+
const readSummaryPayloadOptional = (sql, summaryId) => {
|
|
959
|
+
const row = sql.exec(`
|
|
960
|
+
SELECT payload_json
|
|
961
|
+
FROM _eda_context_summaries
|
|
962
|
+
WHERE summary_id = ?
|
|
963
|
+
`, summaryId).toArray()[0];
|
|
964
|
+
return row === void 0 ? void 0 : Schema.decodeUnknownSync(CompactionSummaryArtifact)(JSON.parse(row.payload_json));
|
|
965
|
+
};
|
|
966
|
+
const readSummaryPayloadFromCreatedSeq = (sql, createdSeq) => {
|
|
967
|
+
const row = sql.exec(`
|
|
968
|
+
SELECT payload_json
|
|
969
|
+
FROM _eda_context_summaries
|
|
970
|
+
WHERE created_seq = ?
|
|
971
|
+
`, createdSeq).toArray()[0];
|
|
972
|
+
if (row === void 0) throw new Error(`SummaryCreated at seq ${createdSeq} missing from _eda_context_summaries`);
|
|
973
|
+
return JSON.parse(row.payload_json);
|
|
974
|
+
};
|
|
975
|
+
/**
|
|
976
|
+
* Maintain bounded storage projections in the same transaction as the event.
|
|
977
|
+
*
|
|
978
|
+
* These rows are durability aids, not a second source of lifecycle truth: event
|
|
979
|
+
* order still comes from `_eda_event_log.seq`, while projections make command
|
|
980
|
+
* lookup and body-sidecar hydration bounded.
|
|
981
|
+
*/
|
|
982
|
+
const applySynchronousProjections = (sql, entry, seq, factJson) => {
|
|
983
|
+
const { event } = entry;
|
|
984
|
+
if (event.namespace !== effectDurableAgentNamespace) return;
|
|
985
|
+
writeCommandProjection(sql, event, seq, factJson);
|
|
986
|
+
writeCommandInputProjection(sql, event, seq);
|
|
987
|
+
writeMessageProjection(sql, event, seq);
|
|
988
|
+
writeSummaryProjection(sql, event, seq);
|
|
989
|
+
writeContextRebaseProjection(sql, event, seq);
|
|
990
|
+
};
|
|
991
|
+
const writeCommandProjection = (sql, event, seq, factJson) => {
|
|
992
|
+
const payload = Schema.decodeUnknownSync(ProjectionPayload)(event.payload);
|
|
993
|
+
switch (event.type) {
|
|
994
|
+
case commandAdmittedEventType: {
|
|
995
|
+
const command = Schema.decodeUnknownSync(AdmittedCommandProjection)(payload.command);
|
|
996
|
+
const commandId = command.commandId;
|
|
997
|
+
if (commandId === void 0) throw new Error("CommandAdmitted missing command.commandId");
|
|
998
|
+
sql.exec(`
|
|
999
|
+
INSERT INTO _eda_command_state (
|
|
1000
|
+
command_id,
|
|
1001
|
+
admitted_seq,
|
|
1002
|
+
status,
|
|
1003
|
+
idempotency_key,
|
|
1004
|
+
payload_json
|
|
1005
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
1006
|
+
ON CONFLICT(command_id) DO UPDATE SET
|
|
1007
|
+
admitted_seq = excluded.admitted_seq,
|
|
1008
|
+
status = excluded.status,
|
|
1009
|
+
idempotency_key = excluded.idempotency_key,
|
|
1010
|
+
payload_json = excluded.payload_json
|
|
1011
|
+
`, commandId, seq, "admitted", command.idempotencyKey ?? null, factJson);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
case commandStartedEventType:
|
|
1015
|
+
writeCommandStatus(sql, payload.commandId, seq, "started", factJson);
|
|
1016
|
+
return;
|
|
1017
|
+
case commandCompletedEventType:
|
|
1018
|
+
writeCommandStatus(sql, payload.commandId, seq, "completed", factJson);
|
|
1019
|
+
return;
|
|
1020
|
+
case commandFailedEventType:
|
|
1021
|
+
writeCommandStatus(sql, payload.commandId, seq, "failed", factJson);
|
|
1022
|
+
return;
|
|
1023
|
+
case commandCancelledEventType:
|
|
1024
|
+
writeCommandStatus(sql, payload.commandId, seq, "cancelled", factJson);
|
|
1025
|
+
return;
|
|
1026
|
+
default: return;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
const writeCommandStatus = (sql, commandId, seq, status, payloadJson) => {
|
|
1030
|
+
if (commandId === void 0) throw new Error(`${status} command event missing commandId`);
|
|
1031
|
+
sql.exec(`
|
|
1032
|
+
INSERT INTO _eda_command_state (
|
|
1033
|
+
command_id,
|
|
1034
|
+
admitted_seq,
|
|
1035
|
+
status,
|
|
1036
|
+
idempotency_key,
|
|
1037
|
+
payload_json
|
|
1038
|
+
) VALUES (?, ?, ?, NULL, ?)
|
|
1039
|
+
ON CONFLICT(command_id) DO UPDATE SET
|
|
1040
|
+
status = excluded.status,
|
|
1041
|
+
payload_json = excluded.payload_json
|
|
1042
|
+
`, commandId, seq, status, payloadJson);
|
|
1043
|
+
};
|
|
1044
|
+
const writeCommandInputProjection = (sql, event, seq) => {
|
|
1045
|
+
if (event.type !== commandAdmittedEventType) return;
|
|
1046
|
+
const encoded = Schema.encodeSync(DurableEventEnvelope)(event);
|
|
1047
|
+
const { command } = Schema.decodeUnknownSync(Schema.Struct({ command: Schema.Record(Schema.String, Schema.Unknown) }))(encoded.payload);
|
|
1048
|
+
const commandId = command?.commandId;
|
|
1049
|
+
if (commandId === void 0) throw new Error("CommandAdmitted missing command.commandId");
|
|
1050
|
+
sql.exec(`
|
|
1051
|
+
INSERT INTO _eda_command_inputs (
|
|
1052
|
+
command_id,
|
|
1053
|
+
admitted_seq,
|
|
1054
|
+
payload_json
|
|
1055
|
+
) VALUES (?, ?, ?)
|
|
1056
|
+
ON CONFLICT(command_id) DO UPDATE SET
|
|
1057
|
+
admitted_seq = excluded.admitted_seq,
|
|
1058
|
+
payload_json = excluded.payload_json
|
|
1059
|
+
`, commandId, seq, stringifyJsonForColumn(command, "_eda_command_inputs.payload_json"));
|
|
1060
|
+
};
|
|
1061
|
+
const contextMessageEventTypes = /* @__PURE__ */ new Set([
|
|
1062
|
+
systemMessageCommittedEventType,
|
|
1063
|
+
userMessageCommittedEventType,
|
|
1064
|
+
steeringMessageQueuedEventType,
|
|
1065
|
+
assistantMessageCommittedEventType,
|
|
1066
|
+
assistantMessageImportedEventType,
|
|
1067
|
+
assistantPartialCommittedEventType
|
|
1068
|
+
]);
|
|
1069
|
+
/** Persist model-visible message bodies with the seq that exposed them to context. */
|
|
1070
|
+
const writeMessageProjection = (sql, event, seq) => {
|
|
1071
|
+
if (!contextMessageEventTypes.has(event.type)) return;
|
|
1072
|
+
const encoded = Schema.encodeSync(DurableEventEnvelope)(event);
|
|
1073
|
+
const { messageId } = Schema.decodeUnknownSync(Schema.Struct({ messageId: Schema.Unknown }))(encoded.payload);
|
|
1074
|
+
if (messageId === void 0) throw new Error(`${event.type} missing messageId`);
|
|
1075
|
+
sql.exec(`
|
|
1076
|
+
INSERT INTO _eda_context_messages (
|
|
1077
|
+
message_id,
|
|
1078
|
+
context_seq,
|
|
1079
|
+
payload_json
|
|
1080
|
+
) VALUES (?, ?, ?)
|
|
1081
|
+
ON CONFLICT(message_id) DO UPDATE SET
|
|
1082
|
+
context_seq = excluded.context_seq,
|
|
1083
|
+
payload_json = excluded.payload_json
|
|
1084
|
+
`, messageId, seq, stringifyJsonForColumn(encoded.payload, "_eda_context_messages.payload_json"));
|
|
1085
|
+
};
|
|
1086
|
+
/**
|
|
1087
|
+
* Persist a cumulative summary artifact without making it the active context.
|
|
1088
|
+
*
|
|
1089
|
+
* `ContextRebased` selects summaries in the framework reduced-state checkpoint;
|
|
1090
|
+
* failed compactions can leave orphaned summaries without changing future context.
|
|
1091
|
+
*/
|
|
1092
|
+
const writeSummaryProjection = (sql, event, seq) => {
|
|
1093
|
+
if (event.type !== summaryCreatedEventType) return;
|
|
1094
|
+
const payload = Schema.decodeUnknownSync(SummaryCreatedPayload)(event.payload);
|
|
1095
|
+
const summaryId = payload.summaryId;
|
|
1096
|
+
if (summaryId === void 0) throw new Error("SummaryCreated missing summaryId");
|
|
1097
|
+
if (payload.summary === void 0) throw new Error("SummaryCreated requires summary payload");
|
|
1098
|
+
const summary = Schema.decodeUnknownSync(CompactionSummaryArtifact)(payload.summary);
|
|
1099
|
+
if (summary.summaryId !== summaryId) throw new Error("SummaryCreated summaryId does not match summary payload");
|
|
1100
|
+
if (summary.compactionId !== payload.compactionId) throw new Error("SummaryCreated compactionId does not match summary payload");
|
|
1101
|
+
sql.exec(`
|
|
1102
|
+
INSERT INTO _eda_context_summaries (
|
|
1103
|
+
summary_id,
|
|
1104
|
+
created_seq,
|
|
1105
|
+
payload_json
|
|
1106
|
+
) VALUES (?, ?, ?)
|
|
1107
|
+
ON CONFLICT(summary_id) DO UPDATE SET
|
|
1108
|
+
created_seq = excluded.created_seq,
|
|
1109
|
+
payload_json = excluded.payload_json
|
|
1110
|
+
`, summaryId, seq, stringifyJsonForColumn(summary, "_eda_context_summaries.payload_json"));
|
|
1111
|
+
};
|
|
1112
|
+
/** Validate that `ContextRebased` points at a committed summary sidecar. */
|
|
1113
|
+
const writeContextRebaseProjection = (sql, event, _seq) => {
|
|
1114
|
+
if (event.type !== contextRebasedEventType) return;
|
|
1115
|
+
const payload = Schema.decodeUnknownSync(ContextRebasedPayload)(event.payload);
|
|
1116
|
+
if (payload.summaryId === void 0) throw new Error("ContextRebased missing summaryId");
|
|
1117
|
+
const summary = readSummaryPayloadOptional(sql, SummaryId.make(String(payload.summaryId)));
|
|
1118
|
+
if (summary === void 0) throw new Error(`ContextRebased references unknown summaryId ${String(payload.summaryId)}`);
|
|
1119
|
+
if (summary.compactionId !== payload.compactionId) throw new Error("ContextRebased summary compactionId does not match event payload");
|
|
1120
|
+
if (summary.retainedFromContextSeq !== payload.retainedFromContextSeq) throw new Error("ContextRebased retained cursor does not match summary payload");
|
|
1121
|
+
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Lookup prior command admission through `_eda_command_state` instead of genesis replay.
|
|
1124
|
+
*
|
|
1125
|
+
* Idempotency key wins over explicit command id so caller-owned retry keys remain
|
|
1126
|
+
* the stable dedupe surface for external ingress.
|
|
1127
|
+
*/
|
|
1128
|
+
const findCommandAdmissionRow = (sql, commandId, idempotencyKey) => {
|
|
1129
|
+
if (idempotencyKey !== void 0) {
|
|
1130
|
+
const row = sql.exec(`
|
|
1131
|
+
SELECT admitted_seq
|
|
1132
|
+
FROM _eda_command_state
|
|
1133
|
+
WHERE idempotency_key = ?
|
|
1134
|
+
`, idempotencyKey).toArray()[0];
|
|
1135
|
+
if (row !== void 0) return row;
|
|
1136
|
+
}
|
|
1137
|
+
if (commandId === void 0) return;
|
|
1138
|
+
return sql.exec(`
|
|
1139
|
+
SELECT admitted_seq
|
|
1140
|
+
FROM _eda_command_state
|
|
1141
|
+
WHERE command_id = ?
|
|
1142
|
+
`, commandId).toArray()[0];
|
|
1143
|
+
};
|
|
1144
|
+
const durableStoreError = (operation, cause) => new EDASessionStoreError({ message: cause instanceof Error ? `${operation}: ${cause.message}` : `${operation}: ${String(cause)}` });
|
|
1145
|
+
//#endregion
|
|
1146
|
+
export { emptyTranscriptEffects as a, publishTranscriptEffects as c, TranscriptRowDecodeError as d, makeTranscriptWindowSource as f, transcriptVersionMetaOf as h, defaultTranscriptSliceLimits as i, reconcileTranscriptSlots as l, readTranscriptSlots as m, makeDurableObjectEventLog as n, hasPendingTranscriptWork as o, readOrMintLogIncarnation as p, carriedDefinition as r, missedSessionAlert as s, DurableObjectSessionStore as t, runTranscriptSlice as u };
|
|
1147
|
+
|
|
1148
|
+
//# sourceMappingURL=durable-object-store-Dct3N6rk.js.map
|