@morlay/session-rdb 0.0.11 → 0.0.12
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/README.md +0 -16
- package/dist/index.d.mts +203 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2030 -0
- package/dist/index.mjs.map +1 -0
- package/dist/invariant.d.mts +8 -0
- package/dist/invariant.d.mts.map +1 -0
- package/dist/invariant.mjs +10 -0
- package/dist/invariant.mjs.map +1 -0
- package/package.json +22 -22
- package/src/adapters/ddl.ts +77 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/to-postgres.ts +91 -0
- package/src/adapters/to-sqlite.ts +79 -0
- package/src/backend.ts +112 -0
- package/src/branch.ts +508 -0
- package/src/entities/events.ts +27 -0
- package/src/entities/index.ts +30 -0
- package/src/entities/persistence-state.ts +10 -0
- package/src/entities/schema-meta.ts +9 -0
- package/src/entities/session-events.ts +29 -0
- package/src/entities/sessions.ts +20 -0
- package/src/entities/types.ts +48 -0
- package/src/import.ts +270 -0
- package/src/index.ts +543 -0
- package/src/invariant.ts +15 -0
- package/src/log.ts +370 -0
- package/src/migrate.ts +137 -0
- package/src/postgres.ts +369 -0
- package/src/schema.ts +234 -0
- package/src/sqlite.ts +412 -0
- package/src/write-guard.ts +27 -0
- package/lib/index.d.mts +0 -558
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -2176
- package/lib/index.mjs.map +0 -1
- package/lib/invariant.d.mts +0 -15
- package/lib/invariant.d.mts.map +0 -1
- package/lib/invariant.mjs +0 -21
- package/lib/invariant.mjs.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
5
|
+
import { PersistenceCoordinator, SessionPersistence, SessionPersistenceRevision } from "@deepseek-ai/dsh-session-persistence";
|
|
6
|
+
import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq, decodeSeqRanges, decodeStorageRecord, encodeSeqRanges, packChunkRuns } from "@deepseek-ai/dsh-session";
|
|
7
|
+
import { and, desc, eq, gte, sql } from "drizzle-orm";
|
|
8
|
+
import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
9
|
+
import { bigint, check as check$1, index as index$1, integer as integer$1, pgSchema, pgTable, serial, text as text$1, unique as unique$1 } from "drizzle-orm/pg-core";
|
|
10
|
+
import { statSync } from "node:fs";
|
|
11
|
+
import { mkdir, open } from "node:fs/promises";
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
13
|
+
import { DatabaseSync } from "node:sqlite";
|
|
14
|
+
import { drizzle as drizzle$1 } from "drizzle-orm/node-sqlite";
|
|
15
|
+
import { SessionBranch, SessionBranchError, balanceRewindPrefix, buildTimeline } from "@morlay/session-branch";
|
|
16
|
+
import { unzipSync } from "fflate";
|
|
17
|
+
//#region src/write-guard.ts
|
|
18
|
+
var WriteGuard = class {
|
|
19
|
+
headSeqs = /* @__PURE__ */ new Map();
|
|
20
|
+
confirmHead(id, head) {
|
|
21
|
+
this.headSeqs.set(id, head);
|
|
22
|
+
}
|
|
23
|
+
assertNoConcurrentWriter(id, storedHead) {
|
|
24
|
+
const known = this.headSeqs.get(id);
|
|
25
|
+
if (known === void 0) {
|
|
26
|
+
if (storedHead !== -1) throw new Error(`session "${id}" has a persisted log this instance has not read; another writer may own it — load the session first`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (known !== storedHead) throw new Error(`session "${id}" was modified by another writer (stored head ${storedHead}, this instance last confirmed head ${known}); concurrent writers on one session are not supported`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/log.ts
|
|
34
|
+
function rowToMeta(row) {
|
|
35
|
+
if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
36
|
+
return {
|
|
37
|
+
version: row.fVersion,
|
|
38
|
+
id: row.fSessionId,
|
|
39
|
+
createdAt: row.fCreatedAt,
|
|
40
|
+
...row.fCwd !== null ? { cwd: row.fCwd } : {},
|
|
41
|
+
...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
|
|
42
|
+
isSeeded: row.fSeedLength !== null,
|
|
43
|
+
...row.fOrigin !== null ? { origin: row.fOrigin } : {},
|
|
44
|
+
...row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function sessionInsertRow(storage, incarnation) {
|
|
48
|
+
const meta = storage.meta;
|
|
49
|
+
return {
|
|
50
|
+
fSessionId: meta.id,
|
|
51
|
+
fHeadEventId: "",
|
|
52
|
+
fHeadSequence: -1,
|
|
53
|
+
fVersion: meta.version,
|
|
54
|
+
fCreatedAt: meta.createdAt,
|
|
55
|
+
fCwd: meta.cwd ?? null,
|
|
56
|
+
fParentSession: meta.parentSession ?? null,
|
|
57
|
+
fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
|
|
58
|
+
fOrigin: meta.origin ?? null,
|
|
59
|
+
fDelegationDepth: meta.delegationDepth ?? null,
|
|
60
|
+
fIncarnation: incarnation,
|
|
61
|
+
fRevision: 0
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function sessionConflictRow(storage) {
|
|
65
|
+
const meta = storage.meta;
|
|
66
|
+
return {
|
|
67
|
+
fVersion: meta.version,
|
|
68
|
+
fCreatedAt: meta.createdAt,
|
|
69
|
+
fCwd: meta.cwd ?? null,
|
|
70
|
+
fParentSession: meta.parentSession ?? null,
|
|
71
|
+
fSeedLength: meta.isSeeded ? storage.inheritedEventCount : null,
|
|
72
|
+
fOrigin: meta.origin ?? null,
|
|
73
|
+
fDelegationDepth: meta.delegationDepth ?? null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function remapSurfaceOp(op, remap) {
|
|
77
|
+
if (op === "append") return op;
|
|
78
|
+
return {
|
|
79
|
+
op: "replace",
|
|
80
|
+
start: SessionSeq(remap(op.start)),
|
|
81
|
+
end: SessionSeq(remap(op.end))
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function remapShadowedRange(range, remap) {
|
|
85
|
+
return {
|
|
86
|
+
start: remap(range.start),
|
|
87
|
+
end: remap(range.end)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function rowToEvent(row, seqMap) {
|
|
91
|
+
const remap = (seq) => seqMap.get(seq) ?? seq;
|
|
92
|
+
const surfaceOp = row.fSurfaceOp !== null ? remapSurfaceOp(JSON.parse(row.fSurfaceOp), remap) : void 0;
|
|
93
|
+
const data = JSON.parse(row.fData);
|
|
94
|
+
if (row.fType === "compaction/summary" || row.fType === "compaction/prune") {
|
|
95
|
+
const metering = data;
|
|
96
|
+
if (metering.shadowedRange !== void 0) metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
type: row.fType,
|
|
100
|
+
seq: row.fSequence,
|
|
101
|
+
time: row.fCreatedAt,
|
|
102
|
+
data,
|
|
103
|
+
...surfaceOp === void 0 ? {} : { surfaceOp }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildSeqMap(rows) {
|
|
107
|
+
const map = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const row of rows) if (!map.has(row.fOriginalSeq)) map.set(row.fOriginalSeq, row.fSequence);
|
|
109
|
+
return map;
|
|
110
|
+
}
|
|
111
|
+
const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
112
|
+
"user/message",
|
|
113
|
+
"assistant/message",
|
|
114
|
+
"tool/result"
|
|
115
|
+
]);
|
|
116
|
+
function recomputeReplaceProvenance(events) {
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
const raw = event;
|
|
119
|
+
const op = raw.surfaceOp;
|
|
120
|
+
if (typeof op !== "object" || op === null || op.op !== "replace") continue;
|
|
121
|
+
const { start, end } = op;
|
|
122
|
+
const refs = [];
|
|
123
|
+
for (const candidate of events) if (candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
|
|
124
|
+
raw.sourceEventSeqs = refs;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function isEventSeqLike(value) {
|
|
128
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
129
|
+
}
|
|
130
|
+
function isDeepEqualJson(a, b) {
|
|
131
|
+
if (a === b) return true;
|
|
132
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
133
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
134
|
+
return a.every((item, i) => isDeepEqualJson(item, b[i]));
|
|
135
|
+
}
|
|
136
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
137
|
+
const aKeys = Object.keys(a);
|
|
138
|
+
const bRecord = b;
|
|
139
|
+
if (aKeys.length !== Object.keys(bRecord).length) return false;
|
|
140
|
+
return aKeys.every((key) => Object.hasOwn(bRecord, key) && isDeepEqualJson(a[key], bRecord[key]));
|
|
141
|
+
}
|
|
142
|
+
function toolResultRewriteContentOnly(original, replacement) {
|
|
143
|
+
const originalData = original.data;
|
|
144
|
+
const replacementData = replacement.data;
|
|
145
|
+
const originalMessage = originalData["message"];
|
|
146
|
+
const replacementMessage = replacementData["message"];
|
|
147
|
+
const originalContent = Array.isArray(originalMessage?.content) ? originalMessage.content : void 0;
|
|
148
|
+
const replacementContent = Array.isArray(replacementMessage?.content) ? replacementMessage.content : void 0;
|
|
149
|
+
if (originalContent === void 0 || replacementContent === void 0) return false;
|
|
150
|
+
return isDeepEqualJson({
|
|
151
|
+
...originalData,
|
|
152
|
+
message: {
|
|
153
|
+
...originalMessage,
|
|
154
|
+
content: [{
|
|
155
|
+
...originalContent[0],
|
|
156
|
+
content: null
|
|
157
|
+
}]
|
|
158
|
+
}
|
|
159
|
+
}, {
|
|
160
|
+
...replacementData,
|
|
161
|
+
message: {
|
|
162
|
+
...replacementMessage,
|
|
163
|
+
content: [{
|
|
164
|
+
...replacementContent[0],
|
|
165
|
+
content: null
|
|
166
|
+
}]
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function findSurfaceRepairs(events) {
|
|
171
|
+
const nodes = [];
|
|
172
|
+
const degradeToAppend = /* @__PURE__ */ new Set();
|
|
173
|
+
const addAppendMarker = /* @__PURE__ */ new Set();
|
|
174
|
+
const clearSurfaceOp = /* @__PURE__ */ new Set();
|
|
175
|
+
for (const event of events) {
|
|
176
|
+
const op = event.surfaceOp;
|
|
177
|
+
if (op === void 0) {
|
|
178
|
+
if (SURFACE_EVENT_TYPES.has(event.type)) {
|
|
179
|
+
addAppendMarker.add(event.seq);
|
|
180
|
+
nodes.push(event.seq);
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (op === "append") {
|
|
185
|
+
if (SURFACE_EVENT_TYPES.has(event.type)) nodes.push(event.seq);
|
|
186
|
+
else clearSurfaceOp.add(event.seq);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!SURFACE_EVENT_TYPES.has(event.type)) {
|
|
190
|
+
clearSurfaceOp.add(event.seq);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const replace = typeof op === "object" && op !== null && !Array.isArray(op) ? op : void 0;
|
|
194
|
+
const start = replace?.["start"];
|
|
195
|
+
const end = replace?.["end"];
|
|
196
|
+
const shapeOk = replace !== void 0 && replace["op"] === "replace" && isEventSeqLike(start) && isEventSeqLike(end);
|
|
197
|
+
const startIdx = shapeOk ? nodes.indexOf(start) : -1;
|
|
198
|
+
const endIdx = shapeOk ? nodes.indexOf(end) : -1;
|
|
199
|
+
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
200
|
+
let rewriteOk = true;
|
|
201
|
+
if (rangeOk && event.type === "tool/result") {
|
|
202
|
+
const shadowed = nodes.slice(startIdx, endIdx + 1);
|
|
203
|
+
if (shadowed.length !== 1) rewriteOk = false;
|
|
204
|
+
else {
|
|
205
|
+
const original = events[shadowed[0]];
|
|
206
|
+
rewriteOk = original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!rangeOk || !rewriteOk) {
|
|
210
|
+
degradeToAppend.add(event.seq);
|
|
211
|
+
nodes.push(event.seq);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
degradeToAppend,
|
|
218
|
+
addAppendMarker,
|
|
219
|
+
clearSurfaceOp
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function repairSurfaceOps(events) {
|
|
223
|
+
const repairs = findSurfaceRepairs(events);
|
|
224
|
+
if (repairs.degradeToAppend.size === 0 && repairs.addAppendMarker.size === 0 && repairs.clearSurfaceOp.size === 0) return;
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
const raw = event;
|
|
227
|
+
if (repairs.degradeToAppend.has(event.seq)) raw.surfaceOp = "append";
|
|
228
|
+
else if (repairs.addAppendMarker.has(event.seq)) raw.surfaceOp = "append";
|
|
229
|
+
else if (repairs.clearSurfaceOp.has(event.seq)) delete raw.surfaceOp;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function scanRows(rows, base = 0, seqMap = /* @__PURE__ */ new Map()) {
|
|
233
|
+
const parsed = rows.map((row) => {
|
|
234
|
+
try {
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
event: rowToEvent(row, seqMap)
|
|
238
|
+
};
|
|
239
|
+
} catch {
|
|
240
|
+
return { ok: false };
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
let lastTurnEnd = -1;
|
|
244
|
+
for (let i = parsed.length - 1; i >= 0; i--) if (parsed[i]?.ok && rows[i]?.fType === "turn/end") {
|
|
245
|
+
lastTurnEnd = i;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const preserved = [];
|
|
249
|
+
for (let i = 0; i < rows.length; i++) {
|
|
250
|
+
const p = parsed[i];
|
|
251
|
+
if (!p?.ok || p.event === void 0) {
|
|
252
|
+
if (i <= lastTurnEnd) throw new Error(`corrupt session log: unparsable committed event at seq ${rows[i]?.fSequence}`);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (p.event.seq !== base + i) {
|
|
256
|
+
if (i <= lastTurnEnd) throw new Error(`corrupt session log: seq gap in committed region (expected ${base + i}, got ${p.event.seq})`);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
preserved.push(p.event);
|
|
260
|
+
}
|
|
261
|
+
return preserved.length < rows.length ? {
|
|
262
|
+
preserved,
|
|
263
|
+
tornFrom: base + preserved.length
|
|
264
|
+
} : { preserved };
|
|
265
|
+
}
|
|
266
|
+
function toStorageRecord(record) {
|
|
267
|
+
const withSeqs = record;
|
|
268
|
+
if (withSeqs.sourceEventSeqs === void 0) return record;
|
|
269
|
+
return {
|
|
270
|
+
...record,
|
|
271
|
+
sourceEventSeqs: encodeSeqRanges(withSeqs.sourceEventSeqs)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function toJsonlArtifact(meta, inheritedEventCount, events) {
|
|
275
|
+
const header = {
|
|
276
|
+
type: "session",
|
|
277
|
+
version: meta.version,
|
|
278
|
+
id: meta.id,
|
|
279
|
+
createdAt: meta.createdAt,
|
|
280
|
+
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
281
|
+
...meta.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
|
|
282
|
+
...meta.isSeeded ? { seedLength: inheritedEventCount } : {},
|
|
283
|
+
...meta.origin === void 0 ? {} : { origin: meta.origin },
|
|
284
|
+
delegationDepth: meta.delegationDepth ?? 0,
|
|
285
|
+
...meta.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
|
|
286
|
+
};
|
|
287
|
+
const lines = [JSON.stringify(header)];
|
|
288
|
+
for (const record of packChunkRuns(events)) lines.push(JSON.stringify(toStorageRecord(record)));
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/entities/types.ts
|
|
293
|
+
function toProperty(name) {
|
|
294
|
+
return name.replace(/_([a-z])/g, (_match, char) => char.toUpperCase());
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/adapters/to-sqlite.ts
|
|
298
|
+
function buildColumn$1(c, tables) {
|
|
299
|
+
let col;
|
|
300
|
+
switch (c.type) {
|
|
301
|
+
case "text":
|
|
302
|
+
col = text(c.name);
|
|
303
|
+
break;
|
|
304
|
+
case "serial":
|
|
305
|
+
col = integer(c.name).primaryKey({ autoIncrement: true });
|
|
306
|
+
break;
|
|
307
|
+
case "integer":
|
|
308
|
+
case "bigint": {
|
|
309
|
+
const built = integer(c.name);
|
|
310
|
+
col = c.primaryKey ? built.primaryKey() : built;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (c.notNull) col = col.notNull();
|
|
315
|
+
if (c.default !== void 0) col = col.default(c.default);
|
|
316
|
+
if (c.unique) col = col.unique();
|
|
317
|
+
if (c.references) {
|
|
318
|
+
const { table, column, onDelete } = c.references;
|
|
319
|
+
col = col.references(() => tables[table][toProperty(column)], { onDelete });
|
|
320
|
+
}
|
|
321
|
+
return col;
|
|
322
|
+
}
|
|
323
|
+
function toSqliteSchema(defs) {
|
|
324
|
+
const tables = {};
|
|
325
|
+
for (const def of defs) {
|
|
326
|
+
const columns = {};
|
|
327
|
+
for (const c of def.columns) columns[toProperty(c.name)] = buildColumn$1(c, tables);
|
|
328
|
+
const extra = (self) => [
|
|
329
|
+
...(def.checks ?? []).map((c) => check(c.name, sql.raw(c.expression))),
|
|
330
|
+
...(def.uniques ?? []).map((u) => unique(u.name).on(...u.columns.map((name) => self[toProperty(name)]))),
|
|
331
|
+
...(def.indexes ?? []).map((i) => index(i.name).on(...i.columns.map((name) => self[toProperty(name)])))
|
|
332
|
+
];
|
|
333
|
+
tables[def.name] = sqliteTable(def.name, columns, extra);
|
|
334
|
+
}
|
|
335
|
+
return tables;
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/adapters/to-postgres.ts
|
|
339
|
+
function buildColumn(c, tables) {
|
|
340
|
+
let col;
|
|
341
|
+
switch (c.type) {
|
|
342
|
+
case "text":
|
|
343
|
+
col = text$1(c.name);
|
|
344
|
+
break;
|
|
345
|
+
case "serial":
|
|
346
|
+
col = serial(c.name).primaryKey();
|
|
347
|
+
break;
|
|
348
|
+
case "integer": {
|
|
349
|
+
const built = integer$1(c.name);
|
|
350
|
+
col = c.primaryKey ? built.primaryKey() : built;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "bigint": {
|
|
354
|
+
const built = bigint(c.name, { mode: "number" });
|
|
355
|
+
col = c.primaryKey ? built.primaryKey() : built;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (c.notNull) col = col.notNull();
|
|
360
|
+
if (c.default !== void 0) col = col.default(c.default);
|
|
361
|
+
if (c.unique) col = col.unique();
|
|
362
|
+
if (c.references) {
|
|
363
|
+
const { table, column, onDelete } = c.references;
|
|
364
|
+
col = col.references(() => tables[table][toProperty(column)], { onDelete });
|
|
365
|
+
}
|
|
366
|
+
return col;
|
|
367
|
+
}
|
|
368
|
+
function toPostgresSchema(defs, schemaName = "public") {
|
|
369
|
+
const tables = {};
|
|
370
|
+
const table = schemaName === "public" ? pgTable : pgSchema(schemaName).table;
|
|
371
|
+
for (const def of defs) {
|
|
372
|
+
const columns = {};
|
|
373
|
+
for (const c of def.columns) columns[toProperty(c.name)] = buildColumn(c, tables);
|
|
374
|
+
const extra = (self) => [
|
|
375
|
+
...(def.checks ?? []).map((c) => check$1(c.name, sql.raw(c.expression))),
|
|
376
|
+
...(def.uniques ?? []).map((u) => unique$1(u.name).on(...u.columns.map((name) => self[toProperty(name)]))),
|
|
377
|
+
...(def.indexes ?? []).map((i) => index$1(i.name).on(...i.columns.map((name) => self[toProperty(name)])))
|
|
378
|
+
];
|
|
379
|
+
tables[def.name] = table(def.name, columns, extra);
|
|
380
|
+
}
|
|
381
|
+
return tables;
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/adapters/ddl.ts
|
|
385
|
+
function sqlType(dialect, type) {
|
|
386
|
+
switch (type) {
|
|
387
|
+
case "serial": return dialect === "sqlite" ? "INTEGER" : "SERIAL";
|
|
388
|
+
case "integer": return "INTEGER";
|
|
389
|
+
case "bigint": return dialect === "sqlite" ? "INTEGER" : "BIGINT";
|
|
390
|
+
case "text": return "TEXT";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function literal(value) {
|
|
394
|
+
return typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value);
|
|
395
|
+
}
|
|
396
|
+
function quote(name) {
|
|
397
|
+
return `"${name}"`;
|
|
398
|
+
}
|
|
399
|
+
function columnSql(dialect, c) {
|
|
400
|
+
let sql = `${quote(c.name)} ${sqlType(dialect, c.type)}`;
|
|
401
|
+
if (c.primaryKey) sql += " PRIMARY KEY";
|
|
402
|
+
if (c.type === "serial" && dialect === "sqlite") sql += " AUTOINCREMENT";
|
|
403
|
+
if (c.notNull) sql += " NOT NULL";
|
|
404
|
+
if (c.default !== void 0) sql += ` DEFAULT ${literal(c.default)}`;
|
|
405
|
+
if (c.unique) sql += " UNIQUE";
|
|
406
|
+
if (c.references) {
|
|
407
|
+
sql += ` REFERENCES ${quote(c.references.table)}(${quote(c.references.column)})`;
|
|
408
|
+
if (c.references.onDelete) sql += ` ON DELETE ${c.references.onDelete.toUpperCase()}`;
|
|
409
|
+
}
|
|
410
|
+
return sql;
|
|
411
|
+
}
|
|
412
|
+
function createTableSql(dialect, def, schema) {
|
|
413
|
+
const parts = def.columns.map((c) => columnSql(dialect, c));
|
|
414
|
+
for (const ck of def.checks ?? []) parts.push(`CHECK (${ck.expression})`);
|
|
415
|
+
for (const u of def.uniques ?? []) parts.push(`UNIQUE (${u.columns.map(quote).join(", ")})`);
|
|
416
|
+
const strict = dialect === "sqlite" ? " STRICT" : "";
|
|
417
|
+
return `CREATE TABLE IF NOT EXISTS ${schema === void 0 || schema === "public" ? quote(def.name) : `${quote(schema)}.${quote(def.name)}`} (\n ${parts.join(",\n ")}\n)${strict}`;
|
|
418
|
+
}
|
|
419
|
+
function createIndexSql(def, name, schema) {
|
|
420
|
+
const idx = def.indexes?.find((i) => i.name === name);
|
|
421
|
+
if (idx === void 0) throw new Error(`unknown index "${name}" on table "${def.name}"`);
|
|
422
|
+
const qualified = schema === void 0 || schema === "public" ? quote(def.name) : `${quote(schema)}.${quote(def.name)}`;
|
|
423
|
+
return `CREATE INDEX IF NOT EXISTS ${quote(idx.name)} ON ${qualified}(${idx.columns.map(quote).join(", ")})`;
|
|
424
|
+
}
|
|
425
|
+
function createTablesSql(dialect, defs, schema) {
|
|
426
|
+
const statements = [];
|
|
427
|
+
for (const def of defs) {
|
|
428
|
+
statements.push(createTableSql(dialect, def, schema));
|
|
429
|
+
for (const idx of def.indexes ?? []) statements.push(createIndexSql(def, idx.name, schema));
|
|
430
|
+
}
|
|
431
|
+
return statements;
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/entities/persistence-state.ts
|
|
435
|
+
const persistenceState = {
|
|
436
|
+
name: "t_persistence_state",
|
|
437
|
+
columns: [{
|
|
438
|
+
name: "f_singleton",
|
|
439
|
+
type: "integer",
|
|
440
|
+
primaryKey: true
|
|
441
|
+
}, {
|
|
442
|
+
name: "f_store_id",
|
|
443
|
+
type: "text",
|
|
444
|
+
notNull: true
|
|
445
|
+
}],
|
|
446
|
+
checks: [{
|
|
447
|
+
name: "ck_persistence_state_singleton",
|
|
448
|
+
expression: "f_singleton = 1"
|
|
449
|
+
}]
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/entities/schema-meta.ts
|
|
453
|
+
const schemaMeta = {
|
|
454
|
+
name: "t_schema_meta",
|
|
455
|
+
columns: [{
|
|
456
|
+
name: "f_key",
|
|
457
|
+
type: "text",
|
|
458
|
+
primaryKey: true
|
|
459
|
+
}, {
|
|
460
|
+
name: "f_value",
|
|
461
|
+
type: "text",
|
|
462
|
+
notNull: true
|
|
463
|
+
}]
|
|
464
|
+
};
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/entities/sessions.ts
|
|
467
|
+
const sessions = {
|
|
468
|
+
name: "t_sessions",
|
|
469
|
+
columns: [
|
|
470
|
+
{
|
|
471
|
+
name: "f_id",
|
|
472
|
+
type: "serial",
|
|
473
|
+
primaryKey: true
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "f_session_id",
|
|
477
|
+
type: "text",
|
|
478
|
+
notNull: true,
|
|
479
|
+
unique: true
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "f_head_event_id",
|
|
483
|
+
type: "text",
|
|
484
|
+
notNull: true,
|
|
485
|
+
default: ""
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "f_head_sequence",
|
|
489
|
+
type: "integer",
|
|
490
|
+
notNull: true,
|
|
491
|
+
default: -1
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "f_version",
|
|
495
|
+
type: "integer",
|
|
496
|
+
notNull: true
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "f_created_at",
|
|
500
|
+
type: "bigint",
|
|
501
|
+
notNull: true
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "f_cwd",
|
|
505
|
+
type: "text"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: "f_parent_session",
|
|
509
|
+
type: "text"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "f_seed_length",
|
|
513
|
+
type: "integer"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "f_origin",
|
|
517
|
+
type: "text"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "f_delegation_depth",
|
|
521
|
+
type: "integer"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "f_incarnation",
|
|
525
|
+
type: "text",
|
|
526
|
+
notNull: true
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "f_revision",
|
|
530
|
+
type: "integer",
|
|
531
|
+
notNull: true
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
};
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/entities/events.ts
|
|
537
|
+
const events = {
|
|
538
|
+
name: "t_events",
|
|
539
|
+
columns: [
|
|
540
|
+
{
|
|
541
|
+
name: "f_id",
|
|
542
|
+
type: "serial",
|
|
543
|
+
primaryKey: true
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: "f_event_id",
|
|
547
|
+
type: "text",
|
|
548
|
+
notNull: true,
|
|
549
|
+
unique: true
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "f_parent_id",
|
|
553
|
+
type: "text",
|
|
554
|
+
notNull: true,
|
|
555
|
+
default: ""
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "f_type",
|
|
559
|
+
type: "text",
|
|
560
|
+
notNull: true,
|
|
561
|
+
default: ""
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "f_kind",
|
|
565
|
+
type: "text",
|
|
566
|
+
notNull: true,
|
|
567
|
+
default: ""
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "f_role",
|
|
571
|
+
type: "text",
|
|
572
|
+
notNull: true,
|
|
573
|
+
default: ""
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "f_name",
|
|
577
|
+
type: "text",
|
|
578
|
+
notNull: true,
|
|
579
|
+
default: ""
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: "f_action_id",
|
|
583
|
+
type: "text",
|
|
584
|
+
notNull: true,
|
|
585
|
+
default: ""
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "f_encoding",
|
|
589
|
+
type: "text",
|
|
590
|
+
notNull: true,
|
|
591
|
+
default: ""
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "f_data",
|
|
595
|
+
type: "text",
|
|
596
|
+
notNull: true
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "f_created_at",
|
|
600
|
+
type: "bigint",
|
|
601
|
+
notNull: true,
|
|
602
|
+
default: 0
|
|
603
|
+
}
|
|
604
|
+
],
|
|
605
|
+
indexes: [
|
|
606
|
+
{
|
|
607
|
+
name: "idx_events_kind",
|
|
608
|
+
columns: ["f_kind"]
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: "idx_events_role",
|
|
612
|
+
columns: ["f_role"]
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "idx_events_name",
|
|
616
|
+
columns: ["f_name"]
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "idx_events_action_id",
|
|
620
|
+
columns: ["f_action_id"]
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
};
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/entities/session-events.ts
|
|
626
|
+
const sessionEvents = {
|
|
627
|
+
name: "t_session_events",
|
|
628
|
+
columns: [
|
|
629
|
+
{
|
|
630
|
+
name: "f_id",
|
|
631
|
+
type: "serial",
|
|
632
|
+
primaryKey: true
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "f_session_id",
|
|
636
|
+
type: "text",
|
|
637
|
+
notNull: true,
|
|
638
|
+
references: {
|
|
639
|
+
table: "t_sessions",
|
|
640
|
+
column: "f_session_id",
|
|
641
|
+
onDelete: "cascade"
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "f_event_id",
|
|
646
|
+
type: "text",
|
|
647
|
+
notNull: true,
|
|
648
|
+
references: {
|
|
649
|
+
table: "t_events",
|
|
650
|
+
column: "f_event_id",
|
|
651
|
+
onDelete: "cascade"
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: "f_sequence",
|
|
656
|
+
type: "integer",
|
|
657
|
+
notNull: true
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: "f_original_seq",
|
|
661
|
+
type: "integer",
|
|
662
|
+
notNull: true
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: "f_surface_op",
|
|
666
|
+
type: "text"
|
|
667
|
+
}
|
|
668
|
+
],
|
|
669
|
+
uniques: [{
|
|
670
|
+
name: "uq_session_events_session_sequence",
|
|
671
|
+
columns: ["f_session_id", "f_sequence"]
|
|
672
|
+
}],
|
|
673
|
+
indexes: [{
|
|
674
|
+
name: "idx_session_events_event_id",
|
|
675
|
+
columns: ["f_event_id"]
|
|
676
|
+
}]
|
|
677
|
+
};
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/entities/index.ts
|
|
680
|
+
const sqliteTableDefs = [
|
|
681
|
+
persistenceState,
|
|
682
|
+
sessions,
|
|
683
|
+
events,
|
|
684
|
+
sessionEvents
|
|
685
|
+
];
|
|
686
|
+
const postgresTableDefs = [
|
|
687
|
+
persistenceState,
|
|
688
|
+
schemaMeta,
|
|
689
|
+
sessions,
|
|
690
|
+
events,
|
|
691
|
+
sessionEvents
|
|
692
|
+
];
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/schema.ts
|
|
695
|
+
const SCHEMA_VERSION = 2;
|
|
696
|
+
const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 1146308688;
|
|
697
|
+
const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"];
|
|
698
|
+
const EVENT_ENCODING = "json";
|
|
699
|
+
const sqliteTables = toSqliteSchema(sqliteTableDefs);
|
|
700
|
+
const tPersistenceState = sqliteTables["t_persistence_state"];
|
|
701
|
+
const tSessions = sqliteTables["t_sessions"];
|
|
702
|
+
const tEvents = sqliteTables["t_events"];
|
|
703
|
+
const tSessionEvents = sqliteTables["t_session_events"];
|
|
704
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
705
|
+
function isEphemeralType(type) {
|
|
706
|
+
return EPHEMERAL_EVENT_TYPES.includes(type);
|
|
707
|
+
}
|
|
708
|
+
function isPersistedEvent(event) {
|
|
709
|
+
return !isEphemeralType(event.type) && event.ignorable !== true;
|
|
710
|
+
}
|
|
711
|
+
function eventKind(event) {
|
|
712
|
+
switch (event.type) {
|
|
713
|
+
case "user/message": return "message";
|
|
714
|
+
case "assistant/message":
|
|
715
|
+
if ((event.data.message?.content)?.some((block) => block?.type === "reasoning")) return "thinking";
|
|
716
|
+
return "message";
|
|
717
|
+
case "turn/start":
|
|
718
|
+
case "turn/end":
|
|
719
|
+
case "step/start":
|
|
720
|
+
case "step/end":
|
|
721
|
+
case "session/end-seed": return "turn";
|
|
722
|
+
case "tool/call":
|
|
723
|
+
case "tool/result":
|
|
724
|
+
case "tool/code-dispatch-start":
|
|
725
|
+
case "tool/code-dispatch": return "tool";
|
|
726
|
+
case "request/header":
|
|
727
|
+
case "request/context": return "request";
|
|
728
|
+
case "model/selection":
|
|
729
|
+
case "permission/preset":
|
|
730
|
+
case "approval/policy":
|
|
731
|
+
case "sandbox/mode":
|
|
732
|
+
case "plan/mode":
|
|
733
|
+
case "agent-preset/selected": return "config";
|
|
734
|
+
case "approval/asked":
|
|
735
|
+
case "approval/decided":
|
|
736
|
+
case "command/run":
|
|
737
|
+
case "command/done":
|
|
738
|
+
case "hook/invoked":
|
|
739
|
+
case "hook/result":
|
|
740
|
+
case "feedback/record": return "audit";
|
|
741
|
+
case "session/title":
|
|
742
|
+
case "session/title-llm-request":
|
|
743
|
+
case "session-log-deepseek/delivery-accepted": return "lifecycle";
|
|
744
|
+
case "agent/inbox/spliced": return "inbox";
|
|
745
|
+
case "compaction/start":
|
|
746
|
+
case "compaction/end":
|
|
747
|
+
case "compaction/summary":
|
|
748
|
+
case "compaction/prune": return "compaction";
|
|
749
|
+
case "llm/retry":
|
|
750
|
+
case "llm/retry-started": return "llm";
|
|
751
|
+
case "subagent/descriptor":
|
|
752
|
+
case "subagent/model-selection-policy": return "subagent";
|
|
753
|
+
case "team/member":
|
|
754
|
+
case "team/task":
|
|
755
|
+
case "team/message/queued":
|
|
756
|
+
case "team/message/delivered": return "team";
|
|
757
|
+
case "tool-workflow/run-start":
|
|
758
|
+
case "tool-workflow/run-end":
|
|
759
|
+
case "tool-workflow/agent-start":
|
|
760
|
+
case "tool-workflow/agent-end": return "workflow";
|
|
761
|
+
case "goal/change": return "goal";
|
|
762
|
+
case "schedule/change": return "schedule";
|
|
763
|
+
case "todo/write": return "todo";
|
|
764
|
+
case "web/deepseek-search-llm-request": return "web";
|
|
765
|
+
default: return "";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function eventDimensions(event) {
|
|
769
|
+
const kind = eventKind(event);
|
|
770
|
+
const type = event.type;
|
|
771
|
+
const data = event.data;
|
|
772
|
+
switch (type) {
|
|
773
|
+
case "user/message": return {
|
|
774
|
+
kind,
|
|
775
|
+
role: "user",
|
|
776
|
+
name: "",
|
|
777
|
+
actionId: ""
|
|
778
|
+
};
|
|
779
|
+
case "assistant/message": return {
|
|
780
|
+
kind,
|
|
781
|
+
role: "assistant",
|
|
782
|
+
name: "",
|
|
783
|
+
actionId: ""
|
|
784
|
+
};
|
|
785
|
+
case "tool/result": return {
|
|
786
|
+
kind,
|
|
787
|
+
role: "tool",
|
|
788
|
+
name: "",
|
|
789
|
+
actionId: data["message"]?.content?.[0]?.toolCallId ?? ""
|
|
790
|
+
};
|
|
791
|
+
case "tool/call": return {
|
|
792
|
+
kind,
|
|
793
|
+
role: "",
|
|
794
|
+
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
795
|
+
actionId: typeof data["callId"] === "string" ? data["callId"] : ""
|
|
796
|
+
};
|
|
797
|
+
case "tool/code-dispatch-start":
|
|
798
|
+
case "tool/code-dispatch": return {
|
|
799
|
+
kind,
|
|
800
|
+
role: "",
|
|
801
|
+
name: "",
|
|
802
|
+
actionId: typeof data["subCallId"] === "string" ? data["subCallId"] : ""
|
|
803
|
+
};
|
|
804
|
+
case "command/run":
|
|
805
|
+
case "command/done": return {
|
|
806
|
+
kind,
|
|
807
|
+
role: "",
|
|
808
|
+
name: type === "command/run" && typeof data["name"] === "string" ? data["name"] : "",
|
|
809
|
+
actionId: typeof data["commandId"] === "string" ? data["commandId"] : ""
|
|
810
|
+
};
|
|
811
|
+
case "approval/asked":
|
|
812
|
+
case "approval/decided": return {
|
|
813
|
+
kind,
|
|
814
|
+
role: "",
|
|
815
|
+
name: "",
|
|
816
|
+
actionId: typeof data["id"] === "string" ? data["id"] : ""
|
|
817
|
+
};
|
|
818
|
+
case "hook/invoked":
|
|
819
|
+
case "hook/result": return {
|
|
820
|
+
kind,
|
|
821
|
+
role: "",
|
|
822
|
+
name: "",
|
|
823
|
+
actionId: typeof data["handlerId"] === "string" ? data["handlerId"] : ""
|
|
824
|
+
};
|
|
825
|
+
case "llm/retry":
|
|
826
|
+
case "llm/retry-started": return {
|
|
827
|
+
kind,
|
|
828
|
+
role: "",
|
|
829
|
+
name: "",
|
|
830
|
+
actionId: typeof data["retryId"] === "string" ? data["retryId"] : ""
|
|
831
|
+
};
|
|
832
|
+
case "tool-workflow/run-start":
|
|
833
|
+
case "tool-workflow/run-end":
|
|
834
|
+
case "tool-workflow/agent-start":
|
|
835
|
+
case "tool-workflow/agent-end": return {
|
|
836
|
+
kind,
|
|
837
|
+
role: "",
|
|
838
|
+
name: "",
|
|
839
|
+
actionId: typeof data["runId"] === "string" ? data["runId"] : ""
|
|
840
|
+
};
|
|
841
|
+
case "todo/write": return {
|
|
842
|
+
kind,
|
|
843
|
+
role: "",
|
|
844
|
+
name: "todos",
|
|
845
|
+
actionId: ""
|
|
846
|
+
};
|
|
847
|
+
case "subagent/descriptor": return {
|
|
848
|
+
kind,
|
|
849
|
+
role: "",
|
|
850
|
+
name: typeof data["label"] === "string" ? data["label"] : "",
|
|
851
|
+
actionId: ""
|
|
852
|
+
};
|
|
853
|
+
default: return {
|
|
854
|
+
kind,
|
|
855
|
+
role: "",
|
|
856
|
+
name: "",
|
|
857
|
+
actionId: ""
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function migrateSqliteV1ToV2(db) {
|
|
862
|
+
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
|
|
863
|
+
if (onDisk === 2) return 0;
|
|
864
|
+
if (onDisk !== 1) throw new Error(`unsupported schema version ${onDisk} (expected 1)`);
|
|
865
|
+
db.exec(`
|
|
866
|
+
CREATE TABLE t_events_v2 (
|
|
867
|
+
f_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
868
|
+
f_event_id TEXT NOT NULL UNIQUE,
|
|
869
|
+
f_parent_id TEXT NOT NULL DEFAULT '',
|
|
870
|
+
f_type TEXT NOT NULL DEFAULT '',
|
|
871
|
+
f_kind TEXT NOT NULL DEFAULT '',
|
|
872
|
+
f_role TEXT NOT NULL DEFAULT '',
|
|
873
|
+
f_name TEXT NOT NULL DEFAULT '',
|
|
874
|
+
f_action_id TEXT NOT NULL DEFAULT '',
|
|
875
|
+
f_encoding TEXT NOT NULL DEFAULT '',
|
|
876
|
+
f_data TEXT NOT NULL,
|
|
877
|
+
f_created_at INTEGER NOT NULL DEFAULT 0
|
|
878
|
+
) STRICT;
|
|
879
|
+
CREATE TABLE t_session_events_v2 (
|
|
880
|
+
f_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
881
|
+
f_session_id TEXT NOT NULL REFERENCES t_sessions(f_session_id) ON DELETE CASCADE,
|
|
882
|
+
f_event_id TEXT NOT NULL REFERENCES t_events_v2(f_event_id) ON DELETE CASCADE,
|
|
883
|
+
f_sequence INTEGER NOT NULL,
|
|
884
|
+
f_original_seq INTEGER NOT NULL,
|
|
885
|
+
f_surface_op TEXT,
|
|
886
|
+
UNIQUE (f_session_id, f_sequence)
|
|
887
|
+
) STRICT;
|
|
888
|
+
CREATE INDEX idx_events_v2_kind ON t_events_v2(f_kind);
|
|
889
|
+
CREATE INDEX idx_events_v2_role ON t_events_v2(f_role);
|
|
890
|
+
CREATE INDEX idx_events_v2_name ON t_events_v2(f_name);
|
|
891
|
+
CREATE INDEX idx_events_v2_action_id ON t_events_v2(f_action_id);
|
|
892
|
+
CREATE INDEX idx_session_events_v2_event_id ON t_session_events_v2(f_event_id);
|
|
893
|
+
`);
|
|
894
|
+
const oldEvents = db.prepare(`SELECT f_event_id, f_parent_id, f_kind, f_data, f_created_at FROM t_events ORDER BY f_id`).all();
|
|
895
|
+
const insertEvent = db.prepare(`
|
|
896
|
+
INSERT INTO t_events_v2
|
|
897
|
+
(f_event_id, f_parent_id, f_type, f_kind, f_role, f_name, f_action_id, f_encoding, f_data, f_created_at)
|
|
898
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'json', ?, ?)
|
|
899
|
+
`);
|
|
900
|
+
for (const row of oldEvents) {
|
|
901
|
+
let data;
|
|
902
|
+
try {
|
|
903
|
+
data = JSON.parse(row.f_data);
|
|
904
|
+
} catch {
|
|
905
|
+
throw new Error(`t_events row ${row.f_event_id} has unparsable f_data; aborting`);
|
|
906
|
+
}
|
|
907
|
+
const dims = eventDimensions({
|
|
908
|
+
type: row.f_kind,
|
|
909
|
+
seq: 0,
|
|
910
|
+
time: row.f_created_at,
|
|
911
|
+
data
|
|
912
|
+
});
|
|
913
|
+
insertEvent.run(row.f_event_id, row.f_parent_id, row.f_kind, dims.kind, dims.role, dims.name, dims.actionId, row.f_data, row.f_created_at);
|
|
914
|
+
}
|
|
915
|
+
const oldBridges = db.prepare(`SELECT se.f_session_id, se.f_event_id, se.f_sequence
|
|
916
|
+
FROM t_session_events se ORDER BY se.f_id`).all();
|
|
917
|
+
const oldEventMeta = new Map(db.prepare(`SELECT f_event_id, f_original_seq, f_surface_op FROM t_events`).all().map((row) => [row.f_event_id, row]));
|
|
918
|
+
const insertBridge = db.prepare(`
|
|
919
|
+
INSERT INTO t_session_events_v2
|
|
920
|
+
(f_session_id, f_event_id, f_sequence, f_original_seq, f_surface_op)
|
|
921
|
+
VALUES (?, ?, ?, ?, ?)
|
|
922
|
+
`);
|
|
923
|
+
for (const row of oldBridges) {
|
|
924
|
+
const meta = oldEventMeta.get(row.f_event_id);
|
|
925
|
+
if (meta === void 0) throw new Error(`bridge row ${row.f_session_id}:${row.f_sequence} references missing event ${row.f_event_id}`);
|
|
926
|
+
insertBridge.run(row.f_session_id, row.f_event_id, row.f_sequence, meta.f_original_seq, meta.f_surface_op);
|
|
927
|
+
}
|
|
928
|
+
db.exec(`
|
|
929
|
+
DROP TABLE t_session_events;
|
|
930
|
+
DROP TABLE t_events;
|
|
931
|
+
ALTER TABLE t_events_v2 RENAME TO t_events;
|
|
932
|
+
ALTER TABLE t_session_events_v2 RENAME TO t_session_events;
|
|
933
|
+
`);
|
|
934
|
+
db.exec(`PRAGMA user_version = 2`);
|
|
935
|
+
return oldEvents.length;
|
|
936
|
+
}
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/sqlite.ts
|
|
939
|
+
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
940
|
+
function enqueueSqliteTx(path, fn) {
|
|
941
|
+
const run = (sqliteTxQueues.get(path) ?? Promise.resolve()).then(fn);
|
|
942
|
+
sqliteTxQueues.set(path, run.then(() => void 0, () => void 0));
|
|
943
|
+
return run;
|
|
944
|
+
}
|
|
945
|
+
async function createDatabaseFile(path) {
|
|
946
|
+
try {
|
|
947
|
+
await (await open(path, "wx", 384)).close();
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (error.code !== "EEXIST") throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
|
|
953
|
+
const db = new DatabaseSync(path);
|
|
954
|
+
try {
|
|
955
|
+
configureDatabase(db, path, journalMode, busyTimeout);
|
|
956
|
+
return db;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
db.close();
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function configureDatabase(db, path, journalMode, busyTimeout) {
|
|
963
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
964
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
965
|
+
drizzle$1({ client: db }).transaction((tx) => {
|
|
966
|
+
const { user_version: onDisk } = tx.get(sql`PRAGMA user_version`);
|
|
967
|
+
const { application_id: applicationId } = tx.get(sql`PRAGMA application_id`);
|
|
968
|
+
const { count: userObjectCount } = tx.get(sql`SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'`);
|
|
969
|
+
if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) throw new Error(`session database at "${path}" has an unversioned schema or application identity`);
|
|
970
|
+
if (onDisk === 1) migrateSqliteV1ToV2(db);
|
|
971
|
+
else if (onDisk !== 0 && onDisk !== 2) throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (2)`);
|
|
972
|
+
if ((onDisk === 2 || onDisk === 1) && applicationId !== 1146308688) throw new Error(`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
|
|
973
|
+
for (const statement of createTablesSql("sqlite", sqliteTableDefs)) tx.run(sql.raw(statement));
|
|
974
|
+
tx.insert(tPersistenceState).values({
|
|
975
|
+
fSingleton: 1,
|
|
976
|
+
fStoreId: randomUUID()
|
|
977
|
+
}).onConflictDoNothing().run();
|
|
978
|
+
if (onDisk === 0) {
|
|
979
|
+
tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
|
|
980
|
+
tx.run(sql.raw(`PRAGMA user_version = 2`));
|
|
981
|
+
}
|
|
982
|
+
}, { behavior: "immediate" });
|
|
983
|
+
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
|
|
984
|
+
}
|
|
985
|
+
var SqliteBackend = class SqliteBackend {
|
|
986
|
+
options;
|
|
987
|
+
kind = "sqlite";
|
|
988
|
+
storeIdentity;
|
|
989
|
+
dbPath = "";
|
|
990
|
+
db;
|
|
991
|
+
constructor(options) {
|
|
992
|
+
this.options = options;
|
|
993
|
+
}
|
|
994
|
+
async open() {
|
|
995
|
+
const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
|
|
996
|
+
this.dbPath = actual;
|
|
997
|
+
if (actual !== ":memory:") {
|
|
998
|
+
await mkdir(dirname(actual), {
|
|
999
|
+
recursive: true,
|
|
1000
|
+
mode: 448
|
|
1001
|
+
});
|
|
1002
|
+
await createDatabaseFile(actual);
|
|
1003
|
+
}
|
|
1004
|
+
await enqueueSqliteTx(actual, async () => {
|
|
1005
|
+
this.db = drizzle$1({ client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
|
|
1006
|
+
});
|
|
1007
|
+
try {
|
|
1008
|
+
const row = this.db.select({ fStoreId: tPersistenceState.fStoreId }).from(tPersistenceState).where(eq(tPersistenceState.fSingleton, 1)).get();
|
|
1009
|
+
/* v8 ignore next -- openDatabase inserts the singleton before returning. */
|
|
1010
|
+
if (row === void 0) throw new Error(`session database at "${actual}" has no store identity`);
|
|
1011
|
+
if (row.fStoreId.length === 0) throw new Error(`session database at "${actual}" has no valid store identity`);
|
|
1012
|
+
if (actual !== ":memory:") {
|
|
1013
|
+
const identity = statSync(actual, { bigint: true });
|
|
1014
|
+
this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
|
|
1015
|
+
} else this.storeIdentity = `memory:store:${row.fStoreId}`;
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
this.db.$client.close();
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async close() {
|
|
1022
|
+
if (this.db === void 0) return;
|
|
1023
|
+
this.db.$client.close();
|
|
1024
|
+
}
|
|
1025
|
+
async getSession(id) {
|
|
1026
|
+
return this.db.select().from(tSessions).where(eq(tSessions.fSessionId, id)).get();
|
|
1027
|
+
}
|
|
1028
|
+
async getSeqMapRows(id) {
|
|
1029
|
+
return this.db.select({
|
|
1030
|
+
fSequence: tSessionEvents.fSequence,
|
|
1031
|
+
fOriginalSeq: tSessionEvents.fOriginalSeq
|
|
1032
|
+
}).from(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).all();
|
|
1033
|
+
}
|
|
1034
|
+
async getEventRows(id, fromSequence) {
|
|
1035
|
+
return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
|
|
1036
|
+
}
|
|
1037
|
+
async listSessions() {
|
|
1038
|
+
return this.db.select().from(tSessions).all();
|
|
1039
|
+
}
|
|
1040
|
+
async transaction(fn) {
|
|
1041
|
+
return enqueueSqliteTx(this.dbPath, async () => {
|
|
1042
|
+
this.db.$client.exec("BEGIN IMMEDIATE");
|
|
1043
|
+
try {
|
|
1044
|
+
const result = await fn(this.tx);
|
|
1045
|
+
this.db.$client.exec("COMMIT");
|
|
1046
|
+
return result;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
/* v8 ignore start */
|
|
1049
|
+
try {
|
|
1050
|
+
this.db.$client.exec("ROLLBACK");
|
|
1051
|
+
} catch {}
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
tx = {
|
|
1057
|
+
upsertSession: (storage, incarnation) => this.upsertSession(storage, incarnation),
|
|
1058
|
+
getHead: (id) => this.getHead(id),
|
|
1059
|
+
getSeedLength: (id) => this.getSeedLength(id),
|
|
1060
|
+
updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
|
|
1061
|
+
insertEvents: (events) => this.insertEvents(events),
|
|
1062
|
+
insertBridges: (rows) => this.insertBridges(rows),
|
|
1063
|
+
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
1064
|
+
bumpRevision: (id) => this.bumpRevision(id),
|
|
1065
|
+
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
1066
|
+
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
|
|
1067
|
+
getLastBridge: (id) => this.getLastBridge(id)
|
|
1068
|
+
};
|
|
1069
|
+
async upsertSession(storage, incarnation) {
|
|
1070
|
+
this.db.insert(tSessions).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
|
|
1071
|
+
target: tSessions.fSessionId,
|
|
1072
|
+
set: sessionConflictRow(storage)
|
|
1073
|
+
}).run();
|
|
1074
|
+
}
|
|
1075
|
+
async getHead(id) {
|
|
1076
|
+
const head = this.db.select({
|
|
1077
|
+
fHeadEventId: tSessions.fHeadEventId,
|
|
1078
|
+
fHeadSequence: tSessions.fHeadSequence
|
|
1079
|
+
}).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
|
|
1080
|
+
/* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
|
|
1081
|
+
if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1082
|
+
return head;
|
|
1083
|
+
}
|
|
1084
|
+
async getSeedLength(id) {
|
|
1085
|
+
const row = this.db.select({ fSeedLength: tSessions.fSeedLength }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
|
|
1086
|
+
/* v8 ignore next -- rewind always materializes the row before reading the seed length */
|
|
1087
|
+
if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1088
|
+
return row.fSeedLength;
|
|
1089
|
+
}
|
|
1090
|
+
async updateSeedLength(id, seedLength) {
|
|
1091
|
+
this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
|
|
1092
|
+
}
|
|
1093
|
+
static INSERT_BATCH_ROWS = 1e3;
|
|
1094
|
+
async insertEvents(events) {
|
|
1095
|
+
if (events.length === 0) return;
|
|
1096
|
+
for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tEvents).values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).run();
|
|
1097
|
+
}
|
|
1098
|
+
async insertBridges(rows) {
|
|
1099
|
+
if (rows.length === 0) return;
|
|
1100
|
+
for (let i = 0; i < rows.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tSessionEvents).values(rows.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).run();
|
|
1101
|
+
}
|
|
1102
|
+
async updateHead(id, headEventId, headSequence) {
|
|
1103
|
+
this.db.update(tSessions).set({
|
|
1104
|
+
fHeadEventId: headEventId,
|
|
1105
|
+
fHeadSequence: headSequence
|
|
1106
|
+
}).where(eq(tSessions.fSessionId, id)).run();
|
|
1107
|
+
}
|
|
1108
|
+
async bumpRevision(id) {
|
|
1109
|
+
this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
|
|
1110
|
+
}
|
|
1111
|
+
async deleteBridgeTail(id, fromSequence) {
|
|
1112
|
+
this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
|
|
1113
|
+
}
|
|
1114
|
+
async getPrevBridge(id, sequence) {
|
|
1115
|
+
return this.db.select({
|
|
1116
|
+
fEventId: tSessionEvents.fEventId,
|
|
1117
|
+
fSequence: tSessionEvents.fSequence
|
|
1118
|
+
}).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get();
|
|
1119
|
+
}
|
|
1120
|
+
async getLastBridge(id) {
|
|
1121
|
+
return this.db.select({
|
|
1122
|
+
fEventId: tSessionEvents.fEventId,
|
|
1123
|
+
fSequence: tSessionEvents.fSequence
|
|
1124
|
+
}).from(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
|
|
1125
|
+
}
|
|
1126
|
+
eventRows() {
|
|
1127
|
+
return this.db.select({
|
|
1128
|
+
fEventId: tSessionEvents.fEventId,
|
|
1129
|
+
fSequence: tSessionEvents.fSequence,
|
|
1130
|
+
fOriginalSeq: tSessionEvents.fOriginalSeq,
|
|
1131
|
+
fType: tEvents.fType,
|
|
1132
|
+
fKind: tEvents.fKind,
|
|
1133
|
+
fRole: tEvents.fRole,
|
|
1134
|
+
fName: tEvents.fName,
|
|
1135
|
+
fActionId: tEvents.fActionId,
|
|
1136
|
+
fCreatedAt: tEvents.fCreatedAt,
|
|
1137
|
+
fData: tEvents.fData,
|
|
1138
|
+
fSurfaceOp: tSessionEvents.fSurfaceOp
|
|
1139
|
+
}).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId));
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/postgres.ts
|
|
1144
|
+
var PostgresBackend = class PostgresBackend {
|
|
1145
|
+
db;
|
|
1146
|
+
options;
|
|
1147
|
+
kind = "postgres";
|
|
1148
|
+
storeIdentity;
|
|
1149
|
+
tables;
|
|
1150
|
+
constructor(db, options) {
|
|
1151
|
+
this.db = db;
|
|
1152
|
+
this.options = options;
|
|
1153
|
+
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
1154
|
+
}
|
|
1155
|
+
async open() {
|
|
1156
|
+
const storeId = await this.db.transaction(async (tx) => {
|
|
1157
|
+
const schema = this.options.schema ?? "public";
|
|
1158
|
+
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
1159
|
+
const metaExists = (await tx.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true;
|
|
1160
|
+
for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) await tx.execute(sql.raw(statement));
|
|
1161
|
+
if (!metaExists) await tx.insert(this.tables["t_schema_meta"]).values([{
|
|
1162
|
+
fKey: "schema_version",
|
|
1163
|
+
fValue: String(2)
|
|
1164
|
+
}, {
|
|
1165
|
+
fKey: "application_id",
|
|
1166
|
+
fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID)
|
|
1167
|
+
}]).execute();
|
|
1168
|
+
const version = await this.readMeta(tx, "schema_version");
|
|
1169
|
+
const applicationId = await this.readMeta(tx, "application_id");
|
|
1170
|
+
if (version === void 0 || applicationId === void 0) throw new Error("session database has an unversioned schema or application identity");
|
|
1171
|
+
if (Number(version) !== 2) throw new Error(`session database has schema version ${version}, incompatible with this build (2)`);
|
|
1172
|
+
if (Number(applicationId) !== 1146308688) throw new Error(`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
|
|
1173
|
+
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
1174
|
+
fSingleton: 1,
|
|
1175
|
+
fStoreId: randomUUID()
|
|
1176
|
+
}).onConflictDoNothing().execute();
|
|
1177
|
+
const storeId = (await tx.select({ fStoreId: this.tables["t_persistence_state"].fStoreId }).from(this.tables["t_persistence_state"]).where(eq(this.tables["t_persistence_state"].fSingleton, 1)).execute())[0]?.fStoreId;
|
|
1178
|
+
if (storeId === void 0 || storeId.length === 0) throw new Error("session database has no valid store identity");
|
|
1179
|
+
return storeId;
|
|
1180
|
+
});
|
|
1181
|
+
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
1182
|
+
}
|
|
1183
|
+
async close() {
|
|
1184
|
+
await this.options.close();
|
|
1185
|
+
}
|
|
1186
|
+
async getSession(id) {
|
|
1187
|
+
return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
1188
|
+
}
|
|
1189
|
+
async getSeqMapRows(id) {
|
|
1190
|
+
return this.db.select({
|
|
1191
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
1192
|
+
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq
|
|
1193
|
+
}).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).execute();
|
|
1194
|
+
}
|
|
1195
|
+
async getEventRows(id, fromSequence) {
|
|
1196
|
+
return (fromSequence === void 0 ? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id)) : this.eventRows(this.db).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence)))).orderBy(this.tables["t_session_events"].fSequence).execute();
|
|
1197
|
+
}
|
|
1198
|
+
async listSessions() {
|
|
1199
|
+
return this.db.select().from(this.tables["t_sessions"]).execute();
|
|
1200
|
+
}
|
|
1201
|
+
async transaction(fn) {
|
|
1202
|
+
return this.db.transaction(async (tx) => fn(this.txFor(tx)));
|
|
1203
|
+
}
|
|
1204
|
+
txFor(tx) {
|
|
1205
|
+
return {
|
|
1206
|
+
upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
|
|
1207
|
+
getHead: (id) => this.getHead(tx, id),
|
|
1208
|
+
getSeedLength: (id) => this.getSeedLength(tx, id),
|
|
1209
|
+
updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
|
|
1210
|
+
insertEvents: (events) => this.insertEvents(tx, events),
|
|
1211
|
+
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
1212
|
+
updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
|
|
1213
|
+
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
1214
|
+
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
1215
|
+
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
|
|
1216
|
+
getLastBridge: (id) => this.getLastBridge(tx, id)
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async readMeta(exec, key) {
|
|
1220
|
+
return (await exec.select({ fValue: this.tables["t_schema_meta"].fValue }).from(this.tables["t_schema_meta"]).where(eq(this.tables["t_schema_meta"].fKey, key)).execute())[0]?.fValue;
|
|
1221
|
+
}
|
|
1222
|
+
async upsertSession(exec, storage, incarnation) {
|
|
1223
|
+
await exec.insert(this.tables["t_sessions"]).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
|
|
1224
|
+
target: this.tables["t_sessions"].fSessionId,
|
|
1225
|
+
set: sessionConflictRow(storage)
|
|
1226
|
+
}).execute();
|
|
1227
|
+
}
|
|
1228
|
+
async getHead(exec, id) {
|
|
1229
|
+
const head = (await exec.select({
|
|
1230
|
+
fHeadEventId: this.tables["t_sessions"].fHeadEventId,
|
|
1231
|
+
fHeadSequence: this.tables["t_sessions"].fHeadSequence
|
|
1232
|
+
}).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
1233
|
+
/* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
|
|
1234
|
+
if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1235
|
+
return head;
|
|
1236
|
+
}
|
|
1237
|
+
async getSeedLength(exec, id) {
|
|
1238
|
+
const row = (await exec.select({ fSeedLength: this.tables["t_sessions"].fSeedLength }).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
1239
|
+
/* v8 ignore next -- rewind always materializes the row before reading the seed length */
|
|
1240
|
+
if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1241
|
+
return row.fSeedLength;
|
|
1242
|
+
}
|
|
1243
|
+
async updateSeedLength(exec, id, seedLength) {
|
|
1244
|
+
await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
1245
|
+
}
|
|
1246
|
+
static INSERT_BATCH_ROWS = 1e3;
|
|
1247
|
+
async insertEvents(exec, events) {
|
|
1248
|
+
if (events.length === 0) return;
|
|
1249
|
+
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_events"]).values(events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).execute();
|
|
1250
|
+
}
|
|
1251
|
+
async insertBridges(exec, rows) {
|
|
1252
|
+
if (rows.length === 0) return;
|
|
1253
|
+
for (let i = 0; i < rows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_session_events"]).values(rows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).execute();
|
|
1254
|
+
}
|
|
1255
|
+
async updateHead(exec, id, headEventId, headSequence) {
|
|
1256
|
+
await exec.update(this.tables["t_sessions"]).set({
|
|
1257
|
+
fHeadEventId: headEventId,
|
|
1258
|
+
fHeadSequence: headSequence
|
|
1259
|
+
}).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
1260
|
+
}
|
|
1261
|
+
async bumpRevision(exec, id) {
|
|
1262
|
+
await exec.update(this.tables["t_sessions"]).set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
1263
|
+
}
|
|
1264
|
+
async deleteBridgeTail(exec, id, fromSequence) {
|
|
1265
|
+
await exec.delete(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence))).execute();
|
|
1266
|
+
}
|
|
1267
|
+
async getPrevBridge(exec, id, sequence) {
|
|
1268
|
+
return (await exec.select({
|
|
1269
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
1270
|
+
fSequence: this.tables["t_session_events"].fSequence
|
|
1271
|
+
}).from(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), eq(this.tables["t_session_events"].fSequence, sequence))).execute())[0];
|
|
1272
|
+
}
|
|
1273
|
+
async getLastBridge(exec, id) {
|
|
1274
|
+
return (await exec.select({
|
|
1275
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
1276
|
+
fSequence: this.tables["t_session_events"].fSequence
|
|
1277
|
+
}).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).orderBy(desc(this.tables["t_session_events"].fSequence)).limit(1).execute())[0];
|
|
1278
|
+
}
|
|
1279
|
+
eventRows(exec) {
|
|
1280
|
+
return exec.select({
|
|
1281
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
1282
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
1283
|
+
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
1284
|
+
fType: this.tables["t_events"].fType,
|
|
1285
|
+
fKind: this.tables["t_events"].fKind,
|
|
1286
|
+
fRole: this.tables["t_events"].fRole,
|
|
1287
|
+
fName: this.tables["t_events"].fName,
|
|
1288
|
+
fActionId: this.tables["t_events"].fActionId,
|
|
1289
|
+
fCreatedAt: this.tables["t_events"].fCreatedAt,
|
|
1290
|
+
fData: this.tables["t_events"].fData,
|
|
1291
|
+
fSurfaceOp: this.tables["t_session_events"].fSurfaceOp
|
|
1292
|
+
}).from(this.tables["t_session_events"]).innerJoin(this.tables["t_events"], eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId));
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/branch.ts
|
|
1297
|
+
function locateTurnEnd(events, atSeq, mode = "after") {
|
|
1298
|
+
const ends = events.filter((event) => event.type === "turn/end").map((event) => event.seq);
|
|
1299
|
+
if (atSeq === void 0) {
|
|
1300
|
+
const last = ends.at(-1);
|
|
1301
|
+
if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
|
|
1302
|
+
return last;
|
|
1303
|
+
}
|
|
1304
|
+
if (mode === "before") {
|
|
1305
|
+
let boundary = -1;
|
|
1306
|
+
for (const seq of ends) if (seq < atSeq) boundary = seq;
|
|
1307
|
+
else break;
|
|
1308
|
+
return boundary;
|
|
1309
|
+
}
|
|
1310
|
+
const firstAfter = ends.find((seq) => seq >= atSeq);
|
|
1311
|
+
if (firstAfter !== void 0) return firstAfter;
|
|
1312
|
+
const lastStart = [...events].reverse().find((event) => event.type === "turn/start");
|
|
1313
|
+
if (lastStart !== void 0 && lastStart.seq <= atSeq) throw new SessionBranchError(`anchor ${atSeq} lies inside an open turn`, "OPEN_TURN");
|
|
1314
|
+
const last = ends.at(-1);
|
|
1315
|
+
if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
|
|
1316
|
+
return last;
|
|
1317
|
+
}
|
|
1318
|
+
function renumber(events, offset) {
|
|
1319
|
+
return events.map((event, index) => ({
|
|
1320
|
+
...event,
|
|
1321
|
+
seq: offset + index
|
|
1322
|
+
}));
|
|
1323
|
+
}
|
|
1324
|
+
function mintSessionId() {
|
|
1325
|
+
return `session-${randomUUID()}`;
|
|
1326
|
+
}
|
|
1327
|
+
function truncateLiveSession(session, newLength) {
|
|
1328
|
+
const s = session;
|
|
1329
|
+
s.log.length = newLength;
|
|
1330
|
+
s.eventsSnapshot = void 0;
|
|
1331
|
+
s.headerFold = void 0;
|
|
1332
|
+
s.headerFoldSeq = 0;
|
|
1333
|
+
s.contextFold = void 0;
|
|
1334
|
+
s.contextFoldSeq = 0;
|
|
1335
|
+
s.derived = [];
|
|
1336
|
+
s.derivedNodes = 0;
|
|
1337
|
+
s.derivedGeneration = 0;
|
|
1338
|
+
s.surfaceManager._state = {
|
|
1339
|
+
nodes: [],
|
|
1340
|
+
replaceGeneration: 0
|
|
1341
|
+
};
|
|
1342
|
+
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
1343
|
+
s.surfaceManager._pendingPlan = void 0;
|
|
1344
|
+
}
|
|
1345
|
+
function replaceLiveSessionLog(session, events) {
|
|
1346
|
+
const s = session;
|
|
1347
|
+
s.log.length = 0;
|
|
1348
|
+
s.log.push(...events);
|
|
1349
|
+
s.eventsSnapshot = void 0;
|
|
1350
|
+
s.headerFold = void 0;
|
|
1351
|
+
s.headerFoldSeq = 0;
|
|
1352
|
+
s.contextFold = void 0;
|
|
1353
|
+
s.contextFoldSeq = 0;
|
|
1354
|
+
s.derived = [];
|
|
1355
|
+
s.derivedNodes = 0;
|
|
1356
|
+
s.derivedGeneration = 0;
|
|
1357
|
+
s.surfaceManager._state = {
|
|
1358
|
+
nodes: [],
|
|
1359
|
+
replaceGeneration: 0
|
|
1360
|
+
};
|
|
1361
|
+
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
1362
|
+
s.surfaceManager._pendingPlan = void 0;
|
|
1363
|
+
}
|
|
1364
|
+
var SessionBranchRdbProvider = class {
|
|
1365
|
+
persistence;
|
|
1366
|
+
live;
|
|
1367
|
+
name = "session-rdb";
|
|
1368
|
+
constructor(persistence, live = {
|
|
1369
|
+
getSession: () => void 0,
|
|
1370
|
+
getAgent: () => void 0,
|
|
1371
|
+
flush: async () => true,
|
|
1372
|
+
setCoordinatorCursor: () => {},
|
|
1373
|
+
setCoordinatorState: () => {},
|
|
1374
|
+
setCoordinatorSeedLength: () => {}
|
|
1375
|
+
}) {
|
|
1376
|
+
this.persistence = persistence;
|
|
1377
|
+
this.live = live;
|
|
1378
|
+
}
|
|
1379
|
+
async readBranchPrefix(id, atSeq, mode = "after", signal) {
|
|
1380
|
+
const { events } = await this.readRawEvents(id, signal);
|
|
1381
|
+
const boundary = locateTurnEnd(events, atSeq, mode);
|
|
1382
|
+
return {
|
|
1383
|
+
seq: boundary,
|
|
1384
|
+
events: events.slice(0, boundary + 1)
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
async readRawEvents(id, signal) {
|
|
1388
|
+
const stored = await this.persistence.loadStored(id, signal);
|
|
1389
|
+
if (stored === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
1390
|
+
return {
|
|
1391
|
+
meta: stored.meta,
|
|
1392
|
+
events: stored.events
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
async forkFrom(sourceId, options = {}, signal) {
|
|
1396
|
+
signal?.throwIfAborted();
|
|
1397
|
+
const { atSeq, anchorMode = "after", seedSuffix = [], childSessionId, meta = {} } = options;
|
|
1398
|
+
const source = await this.persistence.inspect(sourceId, signal);
|
|
1399
|
+
const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
|
|
1400
|
+
const prefix = source.events.slice(0, boundary + 1);
|
|
1401
|
+
const childId = childSessionId ?? mintSessionId();
|
|
1402
|
+
const childMeta = {
|
|
1403
|
+
version: SESSION_FORMAT_VERSION,
|
|
1404
|
+
id: childId,
|
|
1405
|
+
createdAt: meta.createdAt ?? Date.now(),
|
|
1406
|
+
...meta.cwd !== void 0 ? { cwd: meta.cwd } : source.meta.cwd !== void 0 ? { cwd: source.meta.cwd } : {},
|
|
1407
|
+
parentSession: sourceId,
|
|
1408
|
+
isSeeded: true,
|
|
1409
|
+
...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : source.meta.agentPreset !== void 0 ? { agentPreset: source.meta.agentPreset } : {},
|
|
1410
|
+
...meta.origin !== void 0 ? { origin: meta.origin } : {},
|
|
1411
|
+
...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {}
|
|
1412
|
+
};
|
|
1413
|
+
const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
|
|
1414
|
+
const internals = this.persistence.internals();
|
|
1415
|
+
const sourceRows = await internals.backend.getEventRows(sourceId);
|
|
1416
|
+
const sourceEventIds = new Map(sourceRows.map((row) => [row.fOriginalSeq, row.fEventId]));
|
|
1417
|
+
const reuse = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const event of prefix) {
|
|
1419
|
+
const eventId = sourceEventIds.get(event.seq);
|
|
1420
|
+
if (eventId !== void 0) reuse.set(event.seq, eventId);
|
|
1421
|
+
}
|
|
1422
|
+
internals.registerReuseEventIds(childId, reuse);
|
|
1423
|
+
await this.persistence.create(childMeta, prefix.length);
|
|
1424
|
+
if (seed.length > 0) await this.persistence.append(childId, seed);
|
|
1425
|
+
return childId;
|
|
1426
|
+
}
|
|
1427
|
+
async rewind(id, toBoundary, signal) {
|
|
1428
|
+
signal?.throwIfAborted();
|
|
1429
|
+
if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
|
|
1430
|
+
const live = this.live.getSession(id);
|
|
1431
|
+
if (live !== void 0) await this.live.flush(live);
|
|
1432
|
+
const raw = live === void 0 ? await this.readRawEvents(id, signal) : void 0;
|
|
1433
|
+
const inspection = live === void 0 ? void 0 : await this.persistence.inspect(id, signal);
|
|
1434
|
+
const events = live === void 0 ? raw.events : inspection.events;
|
|
1435
|
+
const meta = live === void 0 ? raw.meta : inspection.meta;
|
|
1436
|
+
const boundaryEvent = events[toBoundary];
|
|
1437
|
+
if (toBoundary === -1) {} else if (boundaryEvent === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
|
|
1438
|
+
else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`, "INVALID_BOUNDARY");
|
|
1439
|
+
const rawKeepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
1440
|
+
const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
|
|
1441
|
+
const internals = this.persistence.internals();
|
|
1442
|
+
const denseBoundary = live === void 0 ? keepLength - 1 : events.slice(0, keepLength).filter(isPersistedEvent).length - 1;
|
|
1443
|
+
const newSeedLength = await internals.backend.transaction(async (tx) => {
|
|
1444
|
+
signal?.throwIfAborted();
|
|
1445
|
+
const head = await tx.getHead(id);
|
|
1446
|
+
if (denseBoundary > head.fHeadSequence) throw new SessionBranchError(`rewind boundary ${toBoundary} is beyond the stored head ${head.fHeadSequence}`, "INVALID_BOUNDARY");
|
|
1447
|
+
if (denseBoundary < head.fHeadSequence) {
|
|
1448
|
+
await tx.deleteBridgeTail(id, denseBoundary + 1);
|
|
1449
|
+
const prev = denseBoundary === -1 ? void 0 : await tx.getPrevBridge(id, denseBoundary);
|
|
1450
|
+
if (prev === void 0) await tx.updateHead(id, "", -1);
|
|
1451
|
+
else await tx.updateHead(id, prev.fEventId, prev.fSequence);
|
|
1452
|
+
}
|
|
1453
|
+
const storedSeedLength = await tx.getSeedLength(id);
|
|
1454
|
+
let shrunk = storedSeedLength;
|
|
1455
|
+
if (storedSeedLength !== null && storedSeedLength > denseBoundary + 1) {
|
|
1456
|
+
await tx.updateSeedLength(id, denseBoundary + 1);
|
|
1457
|
+
shrunk = denseBoundary + 1;
|
|
1458
|
+
}
|
|
1459
|
+
await tx.bumpRevision(id);
|
|
1460
|
+
return shrunk;
|
|
1461
|
+
});
|
|
1462
|
+
if (newSeedLength !== null) this.live.setCoordinatorSeedLength(id, newSeedLength);
|
|
1463
|
+
internals.writeGuard.confirmHead(id, denseBoundary);
|
|
1464
|
+
if (live !== void 0) {
|
|
1465
|
+
truncateLiveSession(live, keepLength);
|
|
1466
|
+
const agent = this.live.getAgent(id);
|
|
1467
|
+
if (agent !== void 0) {
|
|
1468
|
+
agent.requestHeaderLogged = false;
|
|
1469
|
+
const lastTurn = live.snapshotEvents().findLast((e) => e.type === "turn/start")?.data.turn ?? 0;
|
|
1470
|
+
const phase = agent.phase;
|
|
1471
|
+
if (phase !== void 0) phase.lastTurn = lastTurn;
|
|
1472
|
+
}
|
|
1473
|
+
this.live.setCoordinatorCursor(id, keepLength);
|
|
1474
|
+
} else if (boundaryEvent?.type === "user/message") this.live.setCoordinatorState(id, keepLength, meta);
|
|
1475
|
+
else await this.persistence.load(id);
|
|
1476
|
+
const row = await internals.backend.getSession(id);
|
|
1477
|
+
if (row === void 0) return {
|
|
1478
|
+
header: {
|
|
1479
|
+
version: SESSION_FORMAT_VERSION,
|
|
1480
|
+
id,
|
|
1481
|
+
createdAt: meta.createdAt,
|
|
1482
|
+
...meta.cwd !== void 0 ? { cwd: meta.cwd } : {},
|
|
1483
|
+
...meta.parentSession !== void 0 ? { parentSession: meta.parentSession } : {},
|
|
1484
|
+
isSeeded: meta.isSeeded,
|
|
1485
|
+
...meta.origin !== void 0 ? { origin: meta.origin } : {},
|
|
1486
|
+
...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {},
|
|
1487
|
+
...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : {}
|
|
1488
|
+
},
|
|
1489
|
+
revision: await internals.readStoredRevision(id) ?? await this.persistence.readStoredRevision(id)
|
|
1490
|
+
};
|
|
1491
|
+
return {
|
|
1492
|
+
header: rowToMeta(row),
|
|
1493
|
+
revision: await internals.readStoredRevision(id)
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
var SessionBranchRdb = class extends SessionBranch {
|
|
1498
|
+
static inject = ["sessionPersistence", "sessions"];
|
|
1499
|
+
constructor(ctx) {
|
|
1500
|
+
super(ctx);
|
|
1501
|
+
}
|
|
1502
|
+
provider = new SessionBranchRdbProvider(this.ctx.sessionPersistence, {
|
|
1503
|
+
getSession: (id) => this.ctx.sessions.get(id),
|
|
1504
|
+
getAgent: (id) => {
|
|
1505
|
+
return this.ctx.get("agents")?.get(id);
|
|
1506
|
+
},
|
|
1507
|
+
flush: (session) => this.ctx.sessions.flush(session),
|
|
1508
|
+
setCoordinatorCursor: (id, cursor) => {
|
|
1509
|
+
const state = this.ctx.sessionPersistence.coordinator?.states?.get(id);
|
|
1510
|
+
if (state !== void 0) state.cursor = cursor;
|
|
1511
|
+
},
|
|
1512
|
+
setCoordinatorState: (id, cursor, meta) => {
|
|
1513
|
+
const states = this.ctx.sessionPersistence.coordinator?.states;
|
|
1514
|
+
if (states === void 0) return;
|
|
1515
|
+
const state = states.get(id);
|
|
1516
|
+
if (state !== void 0) state.cursor = cursor;
|
|
1517
|
+
else states.set(id, {
|
|
1518
|
+
meta,
|
|
1519
|
+
cursor,
|
|
1520
|
+
materialized: true
|
|
1521
|
+
});
|
|
1522
|
+
},
|
|
1523
|
+
setCoordinatorSeedLength: (id, seedLength) => {
|
|
1524
|
+
const state = this.ctx.sessionPersistence.coordinator?.states?.get(id);
|
|
1525
|
+
if (state?.storage !== void 0 && state.storage.inheritedEventCount > seedLength) state.storage = {
|
|
1526
|
+
...state.storage,
|
|
1527
|
+
inheritedEventCount: seedLength
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
readBranchPrefix(id, atSeq, mode, signal) {
|
|
1532
|
+
return this.provider.readBranchPrefix(id, atSeq, mode, signal);
|
|
1533
|
+
}
|
|
1534
|
+
readRawEvents(id, signal) {
|
|
1535
|
+
return this.provider.readRawEvents(id, signal);
|
|
1536
|
+
}
|
|
1537
|
+
forkFrom(sourceId, options, signal) {
|
|
1538
|
+
return this.provider.forkFrom(sourceId, options, signal);
|
|
1539
|
+
}
|
|
1540
|
+
rewind(id, toBoundary, signal) {
|
|
1541
|
+
return this.provider.rewind(id, toBoundary, signal);
|
|
1542
|
+
}
|
|
1543
|
+
syncLiveCursor(sessionId) {
|
|
1544
|
+
const live = this.ctx.sessions.get(sessionId);
|
|
1545
|
+
if (live === void 0) return;
|
|
1546
|
+
const state = this.ctx.sessionPersistence.coordinator?.states?.get(sessionId);
|
|
1547
|
+
if (state === void 0) return;
|
|
1548
|
+
let cursor = state.cursor;
|
|
1549
|
+
while (live.snapshotEvents()[cursor]?.ignorable === true) cursor += 1;
|
|
1550
|
+
state.cursor = cursor;
|
|
1551
|
+
}
|
|
1552
|
+
async timeline(sessionId, signal) {
|
|
1553
|
+
const persistence = this.ctx.sessionPersistence;
|
|
1554
|
+
const snapshots = await persistence.listSnapshots(signal);
|
|
1555
|
+
const readOwnEvents = async (id, fromSeq, s) => {
|
|
1556
|
+
const live = this.ctx.sessions.get(id);
|
|
1557
|
+
if (live !== void 0) return live.snapshotEvents().slice(fromSeq);
|
|
1558
|
+
return (await persistence.readFrom(id, fromSeq, s)).events;
|
|
1559
|
+
};
|
|
1560
|
+
return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/import.ts
|
|
1565
|
+
const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
|
|
1566
|
+
const SESSION_IMPORT_PATH = "/api/session.import";
|
|
1567
|
+
const MAX_IMPORT_ZIP_BYTES = 67108864;
|
|
1568
|
+
function expandProvenanceFromStorage(parsed) {
|
|
1569
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("imported session records must be objects");
|
|
1570
|
+
const record = parsed;
|
|
1571
|
+
if (record.sourceEventSeqs === void 0) return parsed;
|
|
1572
|
+
if (!Number.isSafeInteger(record.seq) || record.seq < 0) throw new TypeError("imported session event seq must be a non-negative safe integer");
|
|
1573
|
+
return {
|
|
1574
|
+
...record,
|
|
1575
|
+
sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq + 1)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function parseJsonlArtifact(content) {
|
|
1579
|
+
const lines = content.split("\n");
|
|
1580
|
+
if (lines.length === 0 || lines[0] === "") throw new Error("imported session log is empty");
|
|
1581
|
+
let header;
|
|
1582
|
+
try {
|
|
1583
|
+
header = JSON.parse(lines[0]);
|
|
1584
|
+
} catch {
|
|
1585
|
+
throw new Error("imported session log has an unparsable header line");
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof header !== "object" || header === null || header["type"] !== "session" || typeof header["id"] !== "string" || typeof header["version"] !== "number" || !Number.isSafeInteger(header["createdAt"]) || header["createdAt"] < 0) throw new Error("imported session log has an invalid header line");
|
|
1588
|
+
const seedLength = header["seedLength"];
|
|
1589
|
+
if (seedLength !== void 0 && (!Number.isSafeInteger(seedLength) || seedLength < 0)) throw new Error("imported session log has an invalid seedLength");
|
|
1590
|
+
const events = [];
|
|
1591
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1592
|
+
const line = lines[i];
|
|
1593
|
+
if (line === void 0 || line === "") continue;
|
|
1594
|
+
let parsed;
|
|
1595
|
+
try {
|
|
1596
|
+
parsed = JSON.parse(line);
|
|
1597
|
+
} catch {
|
|
1598
|
+
throw new Error(`imported session log has an unparsable event line at ${i}`);
|
|
1599
|
+
}
|
|
1600
|
+
for (const event of decodeStorageRecord(expandProvenanceFromStorage(parsed))) events.push(event);
|
|
1601
|
+
}
|
|
1602
|
+
for (let i = 0; i < events.length; i++) if (events[i].seq !== i) throw new Error(`imported session log seq gap at ${i} (got ${events[i].seq}); import requires a dense log`);
|
|
1603
|
+
const origin = header["origin"];
|
|
1604
|
+
const delegationDepth = header["delegationDepth"];
|
|
1605
|
+
const inheritedEventCount = Math.min(seedLength ?? 0, events.length);
|
|
1606
|
+
return {
|
|
1607
|
+
meta: {
|
|
1608
|
+
version: header["version"],
|
|
1609
|
+
id: header["id"],
|
|
1610
|
+
createdAt: header["createdAt"],
|
|
1611
|
+
...typeof header["cwd"] === "string" ? { cwd: header["cwd"] } : {},
|
|
1612
|
+
...typeof header["parentSession"] === "string" ? { parentSession: header["parentSession"] } : {},
|
|
1613
|
+
isSeeded: seedLength !== void 0,
|
|
1614
|
+
...origin === "subagent" ? { origin } : {},
|
|
1615
|
+
...Number.isSafeInteger(delegationDepth) && delegationDepth > 0 ? { delegationDepth } : {},
|
|
1616
|
+
...typeof header["agentPreset"] === "string" ? { agentPreset: header["agentPreset"] } : {}
|
|
1617
|
+
},
|
|
1618
|
+
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
1619
|
+
events
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function parseImportZip(zip) {
|
|
1623
|
+
let entries;
|
|
1624
|
+
try {
|
|
1625
|
+
entries = unzipSync(zip);
|
|
1626
|
+
} catch {
|
|
1627
|
+
throw new Error("imported zip is not a valid ZIP archive");
|
|
1628
|
+
}
|
|
1629
|
+
const artifact = entries[SESSION_LOG_ARTIFACT_FILENAME];
|
|
1630
|
+
if (artifact === void 0) throw new Error(`imported zip is missing ${SESSION_LOG_ARTIFACT_FILENAME}`);
|
|
1631
|
+
return parseJsonlArtifact(new TextDecoder().decode(artifact));
|
|
1632
|
+
}
|
|
1633
|
+
async function persistImport(persistence, branch, imported, targetId, sessions) {
|
|
1634
|
+
const id = targetId ?? `session-${randomUUID()}`;
|
|
1635
|
+
if (targetId !== void 0) {
|
|
1636
|
+
if (branch === void 0) throw new Error("sessionBranch service is unavailable");
|
|
1637
|
+
await branch.rewind(targetId, -1);
|
|
1638
|
+
} else await persistence.create({
|
|
1639
|
+
...imported.meta,
|
|
1640
|
+
id
|
|
1641
|
+
}, imported.inheritedEventCount);
|
|
1642
|
+
if (imported.events.length > 0) await persistence.append(id, imported.events);
|
|
1643
|
+
if (targetId !== void 0) {
|
|
1644
|
+
const live = sessions?.get(targetId);
|
|
1645
|
+
if (live !== void 0) replaceLiveSessionLog(live, imported.events);
|
|
1646
|
+
}
|
|
1647
|
+
return id;
|
|
1648
|
+
}
|
|
1649
|
+
function registerSessionImport(ctx, persistence) {
|
|
1650
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
1651
|
+
const webServer = webCtx.webServer;
|
|
1652
|
+
const connection = webCtx.get("connection");
|
|
1653
|
+
return webCtx.effect(() => webServer.register({
|
|
1654
|
+
kind: "exact",
|
|
1655
|
+
path: SESSION_IMPORT_PATH,
|
|
1656
|
+
handler: async (req, res) => {
|
|
1657
|
+
const rejection = connection.requestRejection(req);
|
|
1658
|
+
if (rejection !== void 0) {
|
|
1659
|
+
res.writeHead(rejection);
|
|
1660
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const chunks = [];
|
|
1664
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1665
|
+
const body = Buffer.concat(chunks);
|
|
1666
|
+
let envelope;
|
|
1667
|
+
try {
|
|
1668
|
+
envelope = JSON.parse(body.toString("utf8"));
|
|
1669
|
+
} catch {
|
|
1670
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1671
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (typeof envelope.zip !== "string" || envelope.zip === "") {
|
|
1675
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1676
|
+
res.end(JSON.stringify({ error: "missing zip field" }));
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (envelope.sessionId !== void 0 && (typeof envelope.sessionId !== "string" || envelope.sessionId === "")) {
|
|
1680
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1681
|
+
res.end(JSON.stringify({ error: "sessionId must be a non-empty string" }));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
let zip;
|
|
1685
|
+
try {
|
|
1686
|
+
zip = Buffer.from(envelope.zip, "base64");
|
|
1687
|
+
} catch {
|
|
1688
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1689
|
+
res.end(JSON.stringify({ error: "zip field is not valid base64" }));
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (zip.byteLength > MAX_IMPORT_ZIP_BYTES) {
|
|
1693
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
1694
|
+
res.end(JSON.stringify({ error: "imported zip exceeds the size limit" }));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
let imported;
|
|
1698
|
+
try {
|
|
1699
|
+
imported = parseImportZip(zip);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1702
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "imported zip is invalid" }));
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const targetId = typeof envelope.sessionId === "string" ? envelope.sessionId : void 0;
|
|
1706
|
+
const branch = webCtx.get("sessionBranch");
|
|
1707
|
+
try {
|
|
1708
|
+
const sessions = webCtx.get("sessions");
|
|
1709
|
+
const id = await persistImport(persistence, branch, imported, targetId, sessions);
|
|
1710
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1711
|
+
res.end(JSON.stringify({ sessionId: id }));
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1714
|
+
if (targetId !== void 0 && /not found/i.test(message)) {
|
|
1715
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
1716
|
+
res.end(JSON.stringify({ error: `session "${targetId}" not found` }));
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
1720
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "import failed" }));
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}), `session-rdb: ${SESSION_IMPORT_PATH} route`);
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
//#endregion
|
|
1728
|
+
//#region src/index.ts
|
|
1729
|
+
var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
|
|
1730
|
+
config;
|
|
1731
|
+
static inject = ["sessions", "settings"];
|
|
1732
|
+
static Config = z.union([z.object({
|
|
1733
|
+
type: z.const("sqlite"),
|
|
1734
|
+
path: z.string().required(),
|
|
1735
|
+
journalMode: z.union([
|
|
1736
|
+
"wal",
|
|
1737
|
+
"delete",
|
|
1738
|
+
"truncate",
|
|
1739
|
+
"persist"
|
|
1740
|
+
]).default("wal"),
|
|
1741
|
+
busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS)
|
|
1742
|
+
}), z.object({
|
|
1743
|
+
type: z.const("postgres"),
|
|
1744
|
+
connectionString: z.string().required(),
|
|
1745
|
+
schema: z.string().default("public")
|
|
1746
|
+
})]);
|
|
1747
|
+
static settingsNs = "session-rdb";
|
|
1748
|
+
name = "session-rdb";
|
|
1749
|
+
supportsRawArtifacts = true;
|
|
1750
|
+
async readRaw(id, signal) {
|
|
1751
|
+
signal?.throwIfAborted();
|
|
1752
|
+
await this.ready;
|
|
1753
|
+
signal?.throwIfAborted();
|
|
1754
|
+
const log = await this.readLog(id, {}, signal);
|
|
1755
|
+
if (log === void 0) return void 0;
|
|
1756
|
+
repairSurfaceOps(log.events);
|
|
1757
|
+
recomputeReplaceProvenance(log.events);
|
|
1758
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
1759
|
+
return {
|
|
1760
|
+
meta: log.meta,
|
|
1761
|
+
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
1762
|
+
filename: "session.jsonl",
|
|
1763
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
backend;
|
|
1767
|
+
storeIdentity;
|
|
1768
|
+
ready;
|
|
1769
|
+
coordinator;
|
|
1770
|
+
writeGuard = new WriteGuard();
|
|
1771
|
+
reuseEventIds = /* @__PURE__ */ new Map();
|
|
1772
|
+
constructor(ctx, config, injectedBackend) {
|
|
1773
|
+
let resolved = config;
|
|
1774
|
+
const settings = ctx.reflect.get("settings");
|
|
1775
|
+
if (settings !== void 0) {
|
|
1776
|
+
const scope = settings.register(SessionPersistenceRdb.settingsNs, SessionPersistenceRdb.Config, { base: config });
|
|
1777
|
+
resolved = scope.get();
|
|
1778
|
+
scope.watch(() => {
|
|
1779
|
+
ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
super(ctx);
|
|
1783
|
+
this.config = config;
|
|
1784
|
+
this.config = resolved;
|
|
1785
|
+
this.backend = injectedBackend ?? createBackend(resolved);
|
|
1786
|
+
this.ready = this.init();
|
|
1787
|
+
this.coordinator = new PersistenceCoordinator(this.ctx, this);
|
|
1788
|
+
new SessionBranchRdb(this.ctx);
|
|
1789
|
+
registerSessionImport(this.ctx, this);
|
|
1790
|
+
}
|
|
1791
|
+
async init() {
|
|
1792
|
+
await this.backend.open();
|
|
1793
|
+
this.storeIdentity = this.backend.storeIdentity;
|
|
1794
|
+
}
|
|
1795
|
+
locate(_meta) {}
|
|
1796
|
+
create(meta, inheritedEventCount) {
|
|
1797
|
+
return this.coordinator.create(meta, inheritedEventCount === void 0 ? void 0 : SessionLogOffset(inheritedEventCount));
|
|
1798
|
+
}
|
|
1799
|
+
append(id, events) {
|
|
1800
|
+
return this.coordinator.append(id, events);
|
|
1801
|
+
}
|
|
1802
|
+
load(id) {
|
|
1803
|
+
return this.coordinator.load(id);
|
|
1804
|
+
}
|
|
1805
|
+
inspect(id, signal) {
|
|
1806
|
+
return this.coordinator.inspect(id, signal);
|
|
1807
|
+
}
|
|
1808
|
+
readFrom(id, fromSeq, signal) {
|
|
1809
|
+
return this.coordinator.readFrom(id, fromSeq, signal);
|
|
1810
|
+
}
|
|
1811
|
+
borrowSession(id, signal) {
|
|
1812
|
+
return this.coordinator.borrowSession(id, signal);
|
|
1813
|
+
}
|
|
1814
|
+
loadStored(id, signal) {
|
|
1815
|
+
return this.readPrefix(id, signal);
|
|
1816
|
+
}
|
|
1817
|
+
async loadStoredFrom(id, fromSeq, signal) {
|
|
1818
|
+
const log = await this.readLog(id, { fromSeq }, signal);
|
|
1819
|
+
if (log === void 0) return void 0;
|
|
1820
|
+
return {
|
|
1821
|
+
meta: log.meta,
|
|
1822
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1823
|
+
events: log.events
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
async readPrefix(id, signal) {
|
|
1827
|
+
const log = await this.readLog(id, {}, signal);
|
|
1828
|
+
if (log === void 0) {
|
|
1829
|
+
this.writeGuard.confirmHead(id, -1);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
1833
|
+
recomputeReplaceProvenance(log.events);
|
|
1834
|
+
return {
|
|
1835
|
+
meta: log.meta,
|
|
1836
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1837
|
+
events: log.events,
|
|
1838
|
+
revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`),
|
|
1839
|
+
...log.tornFrom !== void 0 ? { tornMarker: log.tornFrom } : {}
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
async readStoredRevision(id, signal) {
|
|
1843
|
+
signal?.throwIfAborted();
|
|
1844
|
+
await this.ready;
|
|
1845
|
+
signal?.throwIfAborted();
|
|
1846
|
+
const row = await this.backend.getSession(id);
|
|
1847
|
+
if (row === void 0) return void 0;
|
|
1848
|
+
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
1849
|
+
}
|
|
1850
|
+
async readLog(id, options = {}, signal) {
|
|
1851
|
+
signal?.throwIfAborted();
|
|
1852
|
+
await this.ready;
|
|
1853
|
+
signal?.throwIfAborted();
|
|
1854
|
+
const row = await this.backend.getSession(id);
|
|
1855
|
+
if (row === void 0) return void 0;
|
|
1856
|
+
const meta = rowToMeta(row);
|
|
1857
|
+
let eventRows;
|
|
1858
|
+
let seqMap;
|
|
1859
|
+
if (options.fromSeq === void 0) {
|
|
1860
|
+
eventRows = await this.backend.getEventRows(id);
|
|
1861
|
+
seqMap = buildSeqMap(eventRows);
|
|
1862
|
+
} else {
|
|
1863
|
+
eventRows = await this.backend.getEventRows(id, options.fromSeq);
|
|
1864
|
+
seqMap = buildSeqMap(await this.backend.getSeqMapRows(id));
|
|
1865
|
+
}
|
|
1866
|
+
signal?.throwIfAborted();
|
|
1867
|
+
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0, seqMap);
|
|
1868
|
+
return {
|
|
1869
|
+
meta,
|
|
1870
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
1871
|
+
events: preserved,
|
|
1872
|
+
incarnation: row.fIncarnation,
|
|
1873
|
+
revision: row.fRevision,
|
|
1874
|
+
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
async appendBatch(storage, events, _isMaterialized) {
|
|
1878
|
+
await this.ready;
|
|
1879
|
+
const persisted = events.filter(isPersistedEvent);
|
|
1880
|
+
if (persisted.length === 0) return;
|
|
1881
|
+
const meta = storage.meta;
|
|
1882
|
+
const reuse = this.reuseEventIds.get(meta.id);
|
|
1883
|
+
if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
|
|
1884
|
+
let confirmedHead = -1;
|
|
1885
|
+
await this.backend.transaction(async (tx) => {
|
|
1886
|
+
await tx.upsertSession(storage, randomUUID());
|
|
1887
|
+
const head = await tx.getHead(meta.id);
|
|
1888
|
+
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
1889
|
+
const { headEventId, headSequence } = await appendEventTail(tx, meta, persisted, {
|
|
1890
|
+
parentId: head.fHeadEventId,
|
|
1891
|
+
nextSeq: head.fHeadSequence + 1
|
|
1892
|
+
}, reuse);
|
|
1893
|
+
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
1894
|
+
await tx.bumpRevision(meta.id);
|
|
1895
|
+
confirmedHead = headSequence;
|
|
1896
|
+
});
|
|
1897
|
+
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
1898
|
+
}
|
|
1899
|
+
async commitRepair(storage, tornMarker, closers) {
|
|
1900
|
+
await this.ready;
|
|
1901
|
+
const meta = storage.meta;
|
|
1902
|
+
const persistedClosers = closers.filter(isPersistedEvent);
|
|
1903
|
+
if (tornMarker === void 0 && persistedClosers.length === 0) return;
|
|
1904
|
+
await this.backend.transaction(async (tx) => {
|
|
1905
|
+
if (tornMarker !== void 0) {
|
|
1906
|
+
await tx.deleteBridgeTail(meta.id, tornMarker);
|
|
1907
|
+
const prev = await tx.getPrevBridge(meta.id, tornMarker - 1);
|
|
1908
|
+
if (prev === void 0) await tx.updateHead(meta.id, "", -1);
|
|
1909
|
+
else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
1910
|
+
}
|
|
1911
|
+
if (persistedClosers.length > 0) {
|
|
1912
|
+
const last = await tx.getLastBridge(meta.id);
|
|
1913
|
+
const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
|
|
1914
|
+
parentId: last?.fEventId ?? "",
|
|
1915
|
+
nextSeq: (last?.fSequence ?? -1) + 1
|
|
1916
|
+
});
|
|
1917
|
+
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
1918
|
+
}
|
|
1919
|
+
await tx.bumpRevision(meta.id);
|
|
1920
|
+
});
|
|
1921
|
+
const row = await this.backend.getSession(meta.id);
|
|
1922
|
+
this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
|
|
1923
|
+
}
|
|
1924
|
+
async list(signal) {
|
|
1925
|
+
signal?.throwIfAborted();
|
|
1926
|
+
await this.ready;
|
|
1927
|
+
signal?.throwIfAborted();
|
|
1928
|
+
const rows = await this.backend.listSessions();
|
|
1929
|
+
signal?.throwIfAborted();
|
|
1930
|
+
return rows.map(rowToMeta);
|
|
1931
|
+
}
|
|
1932
|
+
async listSnapshots(signal) {
|
|
1933
|
+
signal?.throwIfAborted();
|
|
1934
|
+
await this.ready;
|
|
1935
|
+
signal?.throwIfAborted();
|
|
1936
|
+
const rows = await this.backend.listSessions();
|
|
1937
|
+
signal?.throwIfAborted();
|
|
1938
|
+
return rows.map((row) => ({
|
|
1939
|
+
header: rowToMeta(row),
|
|
1940
|
+
revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`),
|
|
1941
|
+
inheritedEventCount: row.fSeedLength ?? 0
|
|
1942
|
+
}));
|
|
1943
|
+
}
|
|
1944
|
+
async close() {
|
|
1945
|
+
await this.ready;
|
|
1946
|
+
await this.backend.close();
|
|
1947
|
+
}
|
|
1948
|
+
registerReuseEventIds(childId, map) {
|
|
1949
|
+
this.reuseEventIds.set(childId, new Map(map));
|
|
1950
|
+
}
|
|
1951
|
+
internals() {
|
|
1952
|
+
return {
|
|
1953
|
+
backend: this.backend,
|
|
1954
|
+
writeGuard: this.writeGuard,
|
|
1955
|
+
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount),
|
|
1956
|
+
append: (id, events) => this.append(id, events),
|
|
1957
|
+
load: (id) => this.load(id),
|
|
1958
|
+
inspect: (id, signal) => this.inspect(id, signal),
|
|
1959
|
+
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
1960
|
+
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
1961
|
+
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
1962
|
+
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
function createBackend(config) {
|
|
1967
|
+
if (config.type === "sqlite") return new SqliteBackend({
|
|
1968
|
+
path: config.path,
|
|
1969
|
+
journalMode: config.journalMode ?? "wal",
|
|
1970
|
+
busyTimeout: config.busyTimeout ?? 5e3
|
|
1971
|
+
});
|
|
1972
|
+
const pool = new Pool({ connectionString: config.connectionString });
|
|
1973
|
+
pool.on("error", () => {});
|
|
1974
|
+
return new PostgresBackend(drizzle({ client: pool }), {
|
|
1975
|
+
identityBase: [
|
|
1976
|
+
"postgres",
|
|
1977
|
+
pool.options.host ?? "localhost",
|
|
1978
|
+
String(pool.options.port ?? 5432),
|
|
1979
|
+
pool.options.database ?? "",
|
|
1980
|
+
config.schema ?? "public"
|
|
1981
|
+
].join(":"),
|
|
1982
|
+
schema: config.schema ?? "public",
|
|
1983
|
+
close: () => pool.end()
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
1987
|
+
let parentId = anchor.parentId;
|
|
1988
|
+
let nextSeq = anchor.nextSeq;
|
|
1989
|
+
const eventRows = [];
|
|
1990
|
+
const bridgeRows = [];
|
|
1991
|
+
for (const event of events) {
|
|
1992
|
+
const reusedId = reuse?.get(event.seq);
|
|
1993
|
+
const eventId = reusedId ?? randomUUID();
|
|
1994
|
+
if (reusedId === void 0) {
|
|
1995
|
+
const { kind, role, name, actionId } = eventDimensions(event);
|
|
1996
|
+
eventRows.push({
|
|
1997
|
+
fEventId: eventId,
|
|
1998
|
+
fParentId: parentId,
|
|
1999
|
+
fType: event.type,
|
|
2000
|
+
fKind: kind,
|
|
2001
|
+
fRole: role,
|
|
2002
|
+
fName: name,
|
|
2003
|
+
fActionId: actionId,
|
|
2004
|
+
fEncoding: EVENT_ENCODING,
|
|
2005
|
+
fData: JSON.stringify(event.data),
|
|
2006
|
+
fCreatedAt: event.time
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
|
|
2010
|
+
bridgeRows.push({
|
|
2011
|
+
fSessionId: meta.id,
|
|
2012
|
+
fEventId: eventId,
|
|
2013
|
+
fSequence: nextSeq,
|
|
2014
|
+
fOriginalSeq: event.seq,
|
|
2015
|
+
fSurfaceOp: surfaceOp
|
|
2016
|
+
});
|
|
2017
|
+
parentId = eventId;
|
|
2018
|
+
nextSeq++;
|
|
2019
|
+
}
|
|
2020
|
+
if (eventRows.length > 0) await tx.insertEvents(eventRows);
|
|
2021
|
+
await tx.insertBridges(bridgeRows);
|
|
2022
|
+
return {
|
|
2023
|
+
headEventId: parentId,
|
|
2024
|
+
headSequence: nextSeq - 1
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
//#endregion
|
|
2028
|
+
export { EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
|
|
2029
|
+
|
|
2030
|
+
//# sourceMappingURL=index.mjs.map
|