@morlay/session-rdb 0.0.20 → 0.0.21-alpha.0

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.
Files changed (69) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
  5. package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +28 -11
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1631
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +1 -26
  19. package/dist/testing.mjs +10503 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +28 -29
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +1 -4
  67. package/src/usage.ts +191 -0
  68. package/dist/index-D55G9n4k.d.mts +0 -271
  69. package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
@@ -0,0 +1,1904 @@
1
+ import { C as WriteGuard, S as toPostgresSchema, a as EVENT_ENCODING, c as eventDimensions, i as DEFAULT_BUSY_TIMEOUT_MS, r as createStorageRepository, t as SqliteBackend, x as postgresTableDefs } from "./sqlite-fpvm5Dzs.mjs";
2
+ import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-CnYct2Dv.mjs";
3
+ import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-5JzX9rUq.mjs";
4
+ import { registerSessionImport } from "./import.mjs";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import { randomUUID } from "node:crypto";
7
+ import { Pool } from "pg";
8
+ import { drizzle } from "drizzle-orm/node-postgres";
9
+ import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
10
+ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
11
+ import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
12
+ import { and, desc, eq, gte, inArray, lt, notInArray, sql } from "drizzle-orm";
13
+ import { readdir } from "node:fs/promises";
14
+ import { fileURLToPath } from "node:url";
15
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
16
+ import { balanceRewindPrefix } from "@morlay/session-branch";
17
+ import { strToU8, zip } from "fflate";
18
+ import { SessionQueryEngine, SessionQueryError } from "@deepseek-ai/dsh-session-query";
19
+ import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
20
+ import { Service } from "@deepseek-ai/cordis";
21
+ //#region src/postgres.ts
22
+ const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
23
+ /** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
24
+ function text(value) {
25
+ return typeof value === "string" ? value : null;
26
+ }
27
+ /** pg 的 count/sum 回落成字符串:统一转成有限数。 */
28
+ function numeric(value) {
29
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
30
+ return Number.isFinite(parsed) ? parsed : 0;
31
+ }
32
+ /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
33
+ function totalsOfRow(row) {
34
+ return {
35
+ events: numeric(row["events"]),
36
+ inputTokens: numeric(row["input_tokens"]),
37
+ outputTokens: numeric(row["output_tokens"]),
38
+ cacheReadTokens: numeric(row["cache_read_tokens"]),
39
+ reasoningTokens: numeric(row["reasoning_tokens"]),
40
+ totalTokens: numeric(row["total_tokens"])
41
+ };
42
+ }
43
+ var PostgresBackend = class PostgresBackend {
44
+ db;
45
+ options;
46
+ kind = "postgres";
47
+ storeIdentity;
48
+ tables;
49
+ storage;
50
+ opened;
51
+ resolveOpened;
52
+ rejectOpened;
53
+ txOverride;
54
+ constructor(db, options) {
55
+ this.db = db;
56
+ this.options = options;
57
+ this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
58
+ this.opened = new Promise((resolve, reject) => {
59
+ this.resolveOpened = resolve;
60
+ this.rejectOpened = reject;
61
+ });
62
+ this.opened.catch(() => {});
63
+ this.storage = createStorageRepository({
64
+ db: () => this.opened.then(() => this.txOverride ?? this.db),
65
+ writeAtomically: (fn) => this.db.transaction(async (tx) => {
66
+ const previous = this.txOverride;
67
+ this.txOverride = tx;
68
+ try {
69
+ return await fn();
70
+ } finally {
71
+ this.txOverride = previous;
72
+ }
73
+ }, { isolationLevel: "serializable" }),
74
+ tables: this.tables
75
+ });
76
+ }
77
+ async open() {
78
+ try {
79
+ await this.doOpen();
80
+ this.resolveOpened();
81
+ } catch (error) {
82
+ this.rejectOpened(error);
83
+ throw error;
84
+ }
85
+ }
86
+ async doOpen() {
87
+ const schema = this.options.schema ?? "public";
88
+ const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
89
+ if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
90
+ if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
91
+ }
92
+ await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
93
+ await this.backfillEventUsage();
94
+ const storeId = await this.db.transaction(async (tx) => {
95
+ await tx.insert(this.tables["t_persistence_state"]).values({
96
+ fSingleton: 1,
97
+ fStoreId: randomUUID()
98
+ }).onConflictDoNothing().execute();
99
+ const id = (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;
100
+ if (id === void 0 || id.length === 0) throw new Error("session database has no valid store identity");
101
+ return id;
102
+ });
103
+ this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
104
+ }
105
+ async baselineV2() {
106
+ const dir = (await readdir(postgresMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
107
+ if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/postgres");
108
+ await this.db.execute(sql`
109
+ CREATE SCHEMA IF NOT EXISTS drizzle
110
+ `);
111
+ await this.db.execute(sql`
112
+ CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
113
+ id SERIAL PRIMARY KEY,
114
+ hash text NOT NULL,
115
+ created_at bigint,
116
+ name text,
117
+ applied_at timestamp with time zone DEFAULT now()
118
+ )
119
+ `);
120
+ await this.db.execute(sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`);
121
+ }
122
+ async close() {
123
+ await this.options.close();
124
+ }
125
+ async getSession(id) {
126
+ return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
127
+ }
128
+ async getEventRows(id, fromSequence) {
129
+ 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();
130
+ }
131
+ async getEventTypeAt(id, sequence) {
132
+ const table = this.tables["t_session_events"];
133
+ return (await this.eventRows(this.db).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
134
+ }
135
+ async getEventTypesBefore(id, beforeSequence, limit) {
136
+ const table = this.tables["t_session_events"];
137
+ return await this.eventRows(this.db).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
138
+ }
139
+ async listSessions() {
140
+ return this.db.select().from(this.tables["t_sessions"]).execute();
141
+ }
142
+ async transaction(fn) {
143
+ return this.db.transaction(async (tx) => fn(this.txFor(tx)));
144
+ }
145
+ txFor(tx) {
146
+ return {
147
+ upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
148
+ getHead: (id) => this.getHead(tx, id),
149
+ getSeedLength: (id) => this.getSeedLength(tx, id),
150
+ updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
151
+ insertEvents: (events) => this.insertEvents(tx, events),
152
+ insertBridges: (rows) => this.insertBridges(tx, rows),
153
+ updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
154
+ bumpRevision: (id) => this.bumpRevision(tx, id),
155
+ refreshTitle: (id) => this.refreshTitle(tx, id),
156
+ deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
157
+ getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
158
+ deleteSession: (id) => this.deleteSession(tx, id),
159
+ deleteSessions: (ids) => this.deleteSessions(tx, ids)
160
+ };
161
+ }
162
+ async readMeta(exec, key) {
163
+ 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;
164
+ }
165
+ async upsertSession(exec, storage, incarnation) {
166
+ await exec.insert(this.tables["t_sessions"]).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
167
+ target: this.tables["t_sessions"].fSessionId,
168
+ set: sessionConflictRow(storage)
169
+ }).execute();
170
+ }
171
+ async getHead(exec, id) {
172
+ const head = (await exec.select({
173
+ fHeadEventId: this.tables["t_sessions"].fHeadEventId,
174
+ fHeadSequence: this.tables["t_sessions"].fHeadSequence
175
+ }).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
176
+ if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
177
+ return head;
178
+ }
179
+ async getSeedLength(exec, id) {
180
+ 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];
181
+ if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
182
+ return row.fSeedLength;
183
+ }
184
+ async updateSeedLength(exec, id, seedLength) {
185
+ await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
186
+ }
187
+ static INSERT_BATCH_ROWS = 1e3;
188
+ async insertEvents(exec, events) {
189
+ if (events.length === 0) return;
190
+ 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();
191
+ const usage = events.flatMap((event) => {
192
+ const row = usageRowOf(event);
193
+ return row === void 0 ? [] : [row];
194
+ });
195
+ if (usage.length > 0) await exec.insert(this.tables["t_event_usage"]).values(usage).onConflictDoNothing().execute();
196
+ }
197
+ async insertBridges(exec, rows) {
198
+ if (rows.length === 0) return;
199
+ 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();
200
+ }
201
+ async updateHead(exec, id, headEventId, headSequence) {
202
+ await exec.update(this.tables["t_sessions"]).set({
203
+ fHeadEventId: headEventId,
204
+ fHeadSequence: headSequence
205
+ }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
206
+ }
207
+ async bumpRevision(exec, id) {
208
+ 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();
209
+ }
210
+ async refreshTitle(exec, id) {
211
+ const bridges = this.tables["t_session_events"];
212
+ const entities = this.tables["t_events"];
213
+ const row = (await exec.select({
214
+ fSequence: bridges.fSequence,
215
+ fData: entities.fData
216
+ }).from(bridges).innerJoin(entities, eq(entities.fEventId, bridges.fEventId)).where(and(eq(bridges.fSessionId, id), eq(entities.fType, "session/title"))).orderBy(desc(bridges.fSequence)).limit(1).execute())[0];
217
+ const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
218
+ await exec.update(this.tables["t_sessions"]).set({
219
+ fTitle: title ?? null,
220
+ fTitleSeq: title === void 0 ? null : row.fSequence
221
+ }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
222
+ }
223
+ async deleteBridgeTail(exec, id, fromSequence) {
224
+ 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();
225
+ }
226
+ async deleteSession(exec, id) {
227
+ await exec.delete(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).execute();
228
+ await exec.delete(this.tables["t_workspace_sessions"]).where(eq(this.tables["t_workspace_sessions"].fSessionId, id)).execute();
229
+ await exec.delete(this.tables["t_session_projcache_row"]).where(eq(this.tables["t_session_projcache_row"].fSessionId, id)).execute();
230
+ await exec.delete(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
231
+ }
232
+ /** 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。 */
233
+ async collectOrphans() {
234
+ const tEvents = this.tables["t_events"];
235
+ const tEventUsage = this.tables["t_event_usage"];
236
+ const tSessionEvents = this.tables["t_session_events"];
237
+ const result = await this.db.delete(tEvents).where(notInArray(tEvents.fEventId, this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents))).execute();
238
+ await this.db.delete(tEventUsage).where(notInArray(tEventUsage.fEventId, this.db.select({ fEventId: tEvents.fEventId }).from(tEvents))).execute();
239
+ return result.rowCount ?? 0;
240
+ }
241
+ /** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
242
+ async listOrphanSubagentSessions() {
243
+ const tSessions = this.tables["t_sessions"];
244
+ return (await this.db.execute(sql`
245
+ SELECT s.f_session_id AS id FROM ${tSessions} AS s
246
+ WHERE s.f_origin = 'subagent'
247
+ AND (s.f_parent_session IS NULL
248
+ OR s.f_parent_session NOT IN (SELECT p.f_session_id FROM ${tSessions} AS p))
249
+ `)).rows.map((row) => row.id);
250
+ }
251
+ async deleteSessions(exec, ids) {
252
+ if (ids.length === 0) return 0;
253
+ await exec.delete(this.tables["t_session_events"]).where(inArray(this.tables["t_session_events"].fSessionId, ids)).execute();
254
+ await exec.delete(this.tables["t_workspace_sessions"]).where(inArray(this.tables["t_workspace_sessions"].fSessionId, ids)).execute();
255
+ await exec.delete(this.tables["t_session_projcache_row"]).where(inArray(this.tables["t_session_projcache_row"].fSessionId, ids)).execute();
256
+ return (await exec.delete(this.tables["t_sessions"]).where(inArray(this.tables["t_sessions"].fSessionId, ids)).execute()).rowCount ?? 0;
257
+ }
258
+ async vacuum() {
259
+ await this.db.execute(sql`VACUUM ANALYZE`);
260
+ }
261
+ /** 一次性回填:旧库没写过 `t_event_usage`,建表后按事件行补一次(表非空即跳过)。 */
262
+ async backfillEventUsage() {
263
+ const tEventUsage = this.tables["t_event_usage"];
264
+ const tEvents = this.tables["t_events"];
265
+ const existing = await this.db.execute(sql`SELECT count(*) AS n FROM ${tEventUsage}`);
266
+ if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
267
+ await this.db.execute(sql`
268
+ INSERT INTO ${tEventUsage} (
269
+ f_event_id, f_created_at, f_provider, f_model,
270
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
271
+ SELECT e.f_event_id, e.f_created_at,
272
+ coalesce(e.f_data::json #>> '{data,message,source,provider}',
273
+ e.f_data::json #>> '{message,source,provider}'),
274
+ coalesce(e.f_data::json #>> '{data,message,source,model}',
275
+ e.f_data::json #>> '{message,source,model}'),
276
+ coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
277
+ (e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
278
+ coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
279
+ (e.f_data::json #>> '{usage,outputTokens}')::integer, 0),
280
+ coalesce((e.f_data::json #>> '{data,usage,cacheReadTokens}')::integer,
281
+ (e.f_data::json #>> '{usage,cacheReadTokens}')::integer, 0),
282
+ coalesce((e.f_data::json #>> '{data,usage,reasoningTokens}')::integer,
283
+ (e.f_data::json #>> '{usage,reasoningTokens}')::integer, 0),
284
+ coalesce((e.f_data::json #>> '{data,usage,totalTokens}')::integer,
285
+ (e.f_data::json #>> '{usage,totalTokens}')::integer, 0)
286
+ FROM ${tEvents} e
287
+ WHERE e.f_type = 'assistant/message'
288
+ AND (e.f_data::json #> '{data,usage}' IS NOT NULL
289
+ OR e.f_data::json #> '{usage}' IS NOT NULL)
290
+ ON CONFLICT DO NOTHING
291
+ `);
292
+ }
293
+ /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
294
+ async usageReport(sinceMs) {
295
+ const sinceClause = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
296
+ const tEventUsage = this.tables["t_event_usage"];
297
+ const tSessionEvents = this.tables["t_session_events"];
298
+ const tSessions = this.tables["t_sessions"];
299
+ const buckets = await this.db.execute(sql`
300
+ SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
301
+ u.f_provider AS provider,
302
+ u.f_model AS model,
303
+ EXISTS (SELECT 1 FROM ${tSessionEvents} sb
304
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
305
+ WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
306
+ count(*) AS events,
307
+ sum(u.f_input_tokens) AS input_tokens,
308
+ sum(u.f_output_tokens) AS output_tokens,
309
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
310
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
311
+ sum(u.f_total_tokens) AS total_tokens
312
+ FROM ${tEventUsage} u
313
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
314
+ GROUP BY 1, 2, 3, 4
315
+ `);
316
+ const sessions = await this.db.execute(sql`
317
+ SELECT b.f_session_id AS session_id,
318
+ s.f_title AS title,
319
+ (s.f_origin = 'subagent') AS subagent,
320
+ (s.f_archived_at IS NOT NULL) AS archived,
321
+ count(*) AS events,
322
+ sum(u.f_input_tokens) AS input_tokens,
323
+ sum(u.f_output_tokens) AS output_tokens,
324
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
325
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
326
+ sum(u.f_total_tokens) AS total_tokens
327
+ FROM ${tSessionEvents} b
328
+ JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
329
+ JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
330
+ WHERE 1 = 1${sinceClause}
331
+ GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
332
+ `);
333
+ return {
334
+ buckets: buckets.rows.map((row) => ({
335
+ day: String(row["day"]),
336
+ provider: text(row["provider"]),
337
+ model: text(row["model"]),
338
+ subagent: row["subagent"] === true,
339
+ ...totalsOfRow(row)
340
+ })),
341
+ sessions: sessions.rows.map((row) => ({
342
+ sessionId: String(row["session_id"]),
343
+ title: text(row["title"]),
344
+ subagent: row["subagent"] === true,
345
+ archived: row["archived"] === true,
346
+ ...totalsOfRow(row)
347
+ }))
348
+ };
349
+ }
350
+ async getPrevBridge(exec, id, sequence) {
351
+ return (await exec.select({
352
+ fEventId: this.tables["t_session_events"].fEventId,
353
+ fSequence: this.tables["t_session_events"].fSequence
354
+ }).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];
355
+ }
356
+ eventRows(exec) {
357
+ return exec.select({
358
+ fEventId: this.tables["t_session_events"].fEventId,
359
+ fSequence: this.tables["t_session_events"].fSequence,
360
+ fType: this.tables["t_events"].fType,
361
+ fKind: this.tables["t_events"].fKind,
362
+ fRole: this.tables["t_events"].fRole,
363
+ fName: this.tables["t_events"].fName,
364
+ fActionId: this.tables["t_events"].fActionId,
365
+ fCreatedAt: this.tables["t_events"].fCreatedAt,
366
+ fData: this.tables["t_events"].fData,
367
+ fSurfaceOp: this.tables["t_session_events"].fSurfaceOp
368
+ }).from(this.tables["t_session_events"]).innerJoin(this.tables["t_events"], eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId));
369
+ }
370
+ };
371
+ //#endregion
372
+ //#region src/deletion.ts
373
+ const SESSION_DELETE_PATH = "/api/session.delete";
374
+ const STATUS_OF_DELETION_ERROR = {
375
+ SESSION_NOT_FOUND: 404,
376
+ SESSION_NOT_ARCHIVED: 409,
377
+ SESSION_LIVE: 409
378
+ };
379
+ function registerSessionDeletion(ctx, persistence) {
380
+ ctx.inject(["webServer", "connection"], (webCtx) => {
381
+ const webServer = webCtx.webServer;
382
+ const connection = webCtx.get("connection");
383
+ return webCtx.effect(() => webServer.register({
384
+ kind: "exact",
385
+ path: SESSION_DELETE_PATH,
386
+ handler: async (req, res) => {
387
+ const rejection = connection.requestRejection(req);
388
+ if (rejection !== void 0) {
389
+ res.writeHead(rejection);
390
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
391
+ return;
392
+ }
393
+ if (req.method !== "POST") {
394
+ res.writeHead(405, { "content-type": "application/json" });
395
+ res.end(JSON.stringify({ error: "method not allowed" }));
396
+ return;
397
+ }
398
+ const chunks = [];
399
+ for await (const chunk of req) chunks.push(chunk);
400
+ let envelope;
401
+ try {
402
+ envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
403
+ } catch {
404
+ res.writeHead(400, { "content-type": "application/json" });
405
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
406
+ return;
407
+ }
408
+ if (typeof envelope.sessionId !== "string" || envelope.sessionId === "") {
409
+ res.writeHead(400, { "content-type": "application/json" });
410
+ res.end(JSON.stringify({ error: "missing sessionId field" }));
411
+ return;
412
+ }
413
+ try {
414
+ await persistence.deleteSession(envelope.sessionId);
415
+ } catch (error) {
416
+ if (error instanceof SessionDeletionError) {
417
+ res.writeHead(STATUS_OF_DELETION_ERROR[error.code], { "content-type": "application/json" });
418
+ res.end(JSON.stringify({
419
+ error: error.message,
420
+ code: error.code
421
+ }));
422
+ return;
423
+ }
424
+ res.writeHead(500, { "content-type": "application/json" });
425
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session deletion failed" }));
426
+ return;
427
+ }
428
+ res.writeHead(200, { "content-type": "application/json" });
429
+ res.end(JSON.stringify({ deleted: envelope.sessionId }));
430
+ }
431
+ }));
432
+ });
433
+ }
434
+ //#endregion
435
+ //#region src/export.ts
436
+ const SESSION_EXPORT_PATH = "/api/session.export";
437
+ /** 文件名只保留安全字符:会话 id 来自请求体,会写进 Content-Disposition。 */
438
+ function safeFilename(sessionId) {
439
+ return sessionId.replace(/[^A-Za-z0-9._-]/gu, "_");
440
+ }
441
+ /** fflate 只给回调式异步 API(同步变体被 node/no-sync 禁止)。 */
442
+ function zipBytes(files) {
443
+ return new Promise((resolve, reject) => {
444
+ zip(files, (error, data) => {
445
+ if (error === null) resolve(data);
446
+ else reject(error);
447
+ });
448
+ });
449
+ }
450
+ /** 导出通道:直接把会话日志打成 zip 响应体,与导入通道读同一份 artifact。 */
451
+ function registerSessionExport(ctx, persistence) {
452
+ ctx.inject(["webServer", "connection"], (webCtx) => {
453
+ const webServer = webCtx.webServer;
454
+ const connection = webCtx.get("connection");
455
+ return webCtx.effect(() => webServer.register({
456
+ kind: "exact",
457
+ path: SESSION_EXPORT_PATH,
458
+ handler: async (req, res) => {
459
+ const rejection = connection.requestRejection(req);
460
+ if (rejection !== void 0) {
461
+ res.writeHead(rejection);
462
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
463
+ return;
464
+ }
465
+ if (req.method !== "POST") {
466
+ res.writeHead(405, { "content-type": "application/json" });
467
+ res.end(JSON.stringify({ error: "method not allowed" }));
468
+ return;
469
+ }
470
+ const chunks = [];
471
+ for await (const chunk of req) chunks.push(chunk);
472
+ let envelope;
473
+ try {
474
+ envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
475
+ } catch {
476
+ res.writeHead(400, { "content-type": "application/json" });
477
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
478
+ return;
479
+ }
480
+ if (typeof envelope.sessionId !== "string" || envelope.sessionId === "") {
481
+ res.writeHead(400, { "content-type": "application/json" });
482
+ res.end(JSON.stringify({ error: "missing sessionId field" }));
483
+ return;
484
+ }
485
+ const sessionId = envelope.sessionId;
486
+ try {
487
+ const raw = await persistence.readRaw(sessionId);
488
+ if (raw === void 0) {
489
+ res.writeHead(404, { "content-type": "application/json" });
490
+ res.end(JSON.stringify({ error: `session "${sessionId}" not found` }));
491
+ return;
492
+ }
493
+ const zipFile = await zipBytes({ [raw.filename]: strToU8(raw.content) });
494
+ res.writeHead(200, {
495
+ "content-type": "application/zip",
496
+ "content-length": String(zipFile.byteLength),
497
+ "content-disposition": `attachment; filename="${safeFilename(sessionId)}.zip"`
498
+ });
499
+ res.end(Buffer.from(zipFile));
500
+ } catch (error) {
501
+ res.writeHead(500, { "content-type": "application/json" });
502
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session export failed" }));
503
+ }
504
+ }
505
+ }), `session-rdb: ${SESSION_EXPORT_PATH} route`);
506
+ });
507
+ }
508
+ //#endregion
509
+ //#region src/gc.ts
510
+ const SESSION_GC_PATH = "/api/session.gc";
511
+ /** 让所有运行中的 agent 先退场:GC 要重写事件表,运行中的写路径必须停下来。 */
512
+ async function stopRunningAgents(ctx) {
513
+ const running = ctx.get("agents")?.list() ?? [];
514
+ for (const agent of running) agent.cancel?.({ kind: "user" }, { keepInbox: true });
515
+ await Promise.all(running.map((agent) => agent.whenIdle()));
516
+ return running.length;
517
+ }
518
+ /** GC 通道:停 agent → 回收孤儿事件行 → VACUUM,一次请求完成。 */
519
+ function registerSessionGc(ctx, persistence) {
520
+ ctx.inject(["webServer", "connection"], (webCtx) => {
521
+ const webServer = webCtx.webServer;
522
+ const connection = webCtx.get("connection");
523
+ return webCtx.effect(() => webServer.register({
524
+ kind: "exact",
525
+ path: SESSION_GC_PATH,
526
+ handler: async (req, res) => {
527
+ const rejection = connection.requestRejection(req);
528
+ if (rejection !== void 0) {
529
+ res.writeHead(rejection);
530
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
531
+ return;
532
+ }
533
+ if (req.method !== "POST") {
534
+ res.writeHead(405, { "content-type": "application/json" });
535
+ res.end(JSON.stringify({ error: "method not allowed" }));
536
+ return;
537
+ }
538
+ try {
539
+ const stoppedAgents = await stopRunningAgents(webCtx);
540
+ const orphanSessions = await persistence.collectOrphanSessions();
541
+ const orphanEvents = await persistence.collectOrphans();
542
+ await persistence.vacuum();
543
+ res.writeHead(200, { "content-type": "application/json" });
544
+ res.end(JSON.stringify({
545
+ orphanSessions,
546
+ orphanEvents,
547
+ stoppedAgents,
548
+ vacuumed: true
549
+ }));
550
+ } catch (error) {
551
+ res.writeHead(500, { "content-type": "application/json" });
552
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "gc failed" }));
553
+ }
554
+ }
555
+ }), `session-rdb: ${SESSION_GC_PATH} route`);
556
+ });
557
+ }
558
+ //#endregion
559
+ //#region src/usage.ts
560
+ const SESSION_USAGE_PATH = "/api/session.usage";
561
+ const DAY_MS = 864e5;
562
+ const ROLLING_DAYS = {
563
+ "7d": 7,
564
+ "30d": 30,
565
+ "90d": 90
566
+ };
567
+ function localDayStart(now) {
568
+ const start = new Date(now);
569
+ start.setHours(0, 0, 0, 0);
570
+ return start;
571
+ }
572
+ /**
573
+ * 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
574
+ * @param range - 语义键。
575
+ * @param now - 当前时刻。
576
+ * @returns 起点毫秒时间戳,或 undefined。
577
+ */
578
+ function resolveUsageSince(range, now) {
579
+ switch (range) {
580
+ case "all": return;
581
+ case "day": return localDayStart(now).getTime();
582
+ case "week": {
583
+ const start = localDayStart(now);
584
+ start.setDate(start.getDate() - (start.getDay() + 6) % 7);
585
+ return start.getTime();
586
+ }
587
+ default: return now - ROLLING_DAYS[range] * DAY_MS;
588
+ }
589
+ }
590
+ function parseUsageRange(value) {
591
+ return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
592
+ }
593
+ function emptyTotals() {
594
+ return {
595
+ events: 0,
596
+ inputTokens: 0,
597
+ outputTokens: 0,
598
+ cacheReadTokens: 0,
599
+ reasoningTokens: 0,
600
+ totalTokens: 0
601
+ };
602
+ }
603
+ /** 折叠一组行(桶或会话行)的用量。 */
604
+ function sumUsage(rows) {
605
+ const totals = emptyTotals();
606
+ for (const row of rows) {
607
+ totals.events += row.events;
608
+ totals.inputTokens += row.inputTokens;
609
+ totals.outputTokens += row.outputTokens;
610
+ totals.cacheReadTokens += row.cacheReadTokens;
611
+ totals.reasoningTokens += row.reasoningTokens;
612
+ totals.totalTokens += row.totalTokens;
613
+ }
614
+ return totals;
615
+ }
616
+ /**
617
+ * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
618
+ * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
619
+ */
620
+ function registerSessionUsage(ctx, persistence) {
621
+ ctx.inject(["webServer", "connection"], (webCtx) => {
622
+ const webServer = webCtx.webServer;
623
+ const connection = webCtx.get("connection");
624
+ return webCtx.effect(() => webServer.register({
625
+ kind: "exact",
626
+ path: SESSION_USAGE_PATH,
627
+ handler: async (req, res) => {
628
+ const rejection = connection.requestRejection(req);
629
+ if (rejection !== void 0) {
630
+ res.writeHead(rejection);
631
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
632
+ return;
633
+ }
634
+ if (req.method !== "POST") {
635
+ res.writeHead(405, { "content-type": "application/json" });
636
+ res.end(JSON.stringify({ error: "method not allowed" }));
637
+ return;
638
+ }
639
+ const chunks = [];
640
+ for await (const chunk of req) chunks.push(chunk);
641
+ const raw = Buffer.concat(chunks).toString("utf8");
642
+ let envelope = {};
643
+ if (raw !== "") try {
644
+ envelope = JSON.parse(raw);
645
+ } catch {
646
+ res.writeHead(400, { "content-type": "application/json" });
647
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
648
+ return;
649
+ }
650
+ try {
651
+ const aggregate = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
652
+ const totals = sumUsage(aggregate.buckets);
653
+ const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
654
+ const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
655
+ res.writeHead(200, { "content-type": "application/json" });
656
+ res.end(JSON.stringify({
657
+ totals,
658
+ subagent,
659
+ human,
660
+ ...aggregate
661
+ }));
662
+ } catch (error) {
663
+ res.writeHead(500, { "content-type": "application/json" });
664
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
665
+ }
666
+ }
667
+ }), `session-rdb: ${SESSION_USAGE_PATH} route`);
668
+ });
669
+ }
670
+ //#endregion
671
+ //#region src/session-query.ts
672
+ var SessionQueryRdb = class extends SessionQueryEngine {
673
+ constructor(ctx, config = {}) {
674
+ super(ctx, config);
675
+ }
676
+ async searchSessions() {
677
+ throw searchDisabled();
678
+ }
679
+ async searchEvents() {
680
+ throw searchDisabled();
681
+ }
682
+ };
683
+ function searchDisabled() {
684
+ return new SessionQueryError("session search is disabled: this deployment serves session queries from the rdb backend without a full-text index", "SESSION_QUERY_SEARCH_DISABLED");
685
+ }
686
+ //#endregion
687
+ //#region src/storage-takeover/projection-cache.ts
688
+ const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
689
+ const PREDECESSOR_TITLE_KEY = "title";
690
+ var SessionProjectionCacheRdb = class extends Service {
691
+ static inject = ["sessionProjections", "sessions"];
692
+ records = /* @__PURE__ */ new Map();
693
+ dirty = /* @__PURE__ */ new Map();
694
+ directReads;
695
+ config;
696
+ repository;
697
+ ready;
698
+ constructor(ctx, options) {
699
+ super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
700
+ this.config = options;
701
+ this.repository = options.repository;
702
+ this.ready = options.ready;
703
+ this.directReads = options.repository.readProjcacheDirect !== void 0;
704
+ }
705
+ async [Service.init]() {
706
+ this.installWritePath();
707
+ await this.ready;
708
+ const pruned = await this.repository.pruneStaleProjcache();
709
+ if (pruned > 0) this.ctx.logger.info(`session projection cache: pruned stale snapshot(s) for ${String(pruned)} session(s)`);
710
+ if (!this.directReads) for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
711
+ }
712
+ lookup(id) {
713
+ if (this.directReads) return this.repository.readProjcacheDirect?.(id);
714
+ return this.records.get(id);
715
+ }
716
+ recordFor(id, expected) {
717
+ const record = this.lookup(id);
718
+ if (record === void 0) return void 0;
719
+ return identityMatches(record.identity, expected) ? record : void 0;
720
+ }
721
+ cachedSnapshot(meta, inheritedEventCount, keys) {
722
+ const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
723
+ const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
724
+ return this.withDirectTitle(meta.id, keys, snapshot);
725
+ }
726
+ withDirectTitle(id, keys, snapshot) {
727
+ if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
728
+ if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
729
+ const direct = this.repository.readSessionTitleDirect?.(id);
730
+ if (direct === void 0) return snapshot;
731
+ const values = {
732
+ ...snapshot?.values,
733
+ [PREDECESSOR_TITLE_KEY]: direct.title
734
+ };
735
+ return {
736
+ asOfSeq: snapshot === void 0 ? direct.seq : Math.min(snapshot.asOfSeq, direct.seq),
737
+ values
738
+ };
739
+ }
740
+ cachedPredecessorTitle(meta, inheritedEventCount) {
741
+ const expected = identityOf(meta, inheritedEventCount);
742
+ const record = this.lookup(meta.id);
743
+ if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return;
744
+ const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
745
+ return title === void 0 ? void 0 : {
746
+ ...title,
747
+ asOfSeq: -1
748
+ };
749
+ }
750
+ viewRecord(record, keys) {
751
+ const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
752
+ const servedKeys = Object.keys(values);
753
+ if (servedKeys.length === 0) return void 0;
754
+ const firstKey = servedKeys[0];
755
+ let asOfSeq = record.rows[firstKey].seq;
756
+ for (const key of servedKeys.slice(1)) {
757
+ const row = record.rows[key];
758
+ if (row.seq < asOfSeq) asOfSeq = row.seq;
759
+ }
760
+ return {
761
+ asOfSeq,
762
+ values
763
+ };
764
+ }
765
+ hydratePrepared(session, events) {
766
+ const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
767
+ if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
768
+ try {
769
+ return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
770
+ } catch {
771
+ return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
772
+ }
773
+ }
774
+ async write(session) {
775
+ const rows = this.ctx.sessionProjections.checkpoint(session);
776
+ this.markClean(session);
777
+ if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
778
+ await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
779
+ }
780
+ async truncateTo(meta, inheritedEventCount, headSeq) {
781
+ const identity = identityOf(meta, inheritedEventCount);
782
+ const record = this.recordFor(meta.id, identity);
783
+ if (record === void 0) return;
784
+ const rows = Object.fromEntries(Object.entries(record.rows).filter(([, row]) => row.seq <= headSeq));
785
+ await this.put(meta.id, identity, rows);
786
+ }
787
+ coldSnapshot(meta, inheritedEventCount, events) {
788
+ const identity = identityOf(meta, inheritedEventCount);
789
+ const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
790
+ this.put(meta.id, identity, restored.checkpoint).catch((error) => {
791
+ this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
792
+ });
793
+ return restored.snapshot;
794
+ }
795
+ installWritePath() {
796
+ this.ctx.on("session/event", (session, event) => {
797
+ if (event.type === "turn/end") {
798
+ this.flushSoft(session, "turn/end");
799
+ return;
800
+ }
801
+ const state = this.dirty.get(session) ?? {
802
+ pending: 0,
803
+ timer: void 0
804
+ };
805
+ this.dirty.set(session, state);
806
+ state.pending += 1;
807
+ if (state.pending >= this.config.writeEveryEvents) {
808
+ this.flushSoft(session, "count threshold");
809
+ return;
810
+ }
811
+ state.timer ??= setTimeout(() => {
812
+ this.flushSoft(session, "interval");
813
+ }, this.config.writeIntervalMs);
814
+ });
815
+ this.ctx.on("session/created", (session) => {
816
+ this.flushSoft(session, "create");
817
+ });
818
+ this.ctx.on("session/disposed", (session) => {
819
+ this.flushSoft(session, "detach");
820
+ this.markClean(session);
821
+ this.dirty.delete(session);
822
+ });
823
+ this.ctx.effect(() => () => {
824
+ for (const state of this.dirty.values()) if (state.timer !== void 0) clearTimeout(state.timer);
825
+ this.dirty.clear();
826
+ }, "sessionProjectionCacheRdb.timers");
827
+ }
828
+ async flushSoft(session, trigger) {
829
+ try {
830
+ await this.write(session);
831
+ } catch (error) {
832
+ this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
833
+ }
834
+ }
835
+ markClean(session) {
836
+ const state = this.dirty.get(session);
837
+ if (state === void 0) return;
838
+ state.pending = 0;
839
+ if (state.timer !== void 0) {
840
+ clearTimeout(state.timer);
841
+ state.timer = void 0;
842
+ }
843
+ }
844
+ async put(id, identity, rows) {
845
+ const detached = detachJson(rows);
846
+ await this.repository.putProjcache(id, detached);
847
+ if (!this.directReads) this.records.set(id, {
848
+ sessionId: id,
849
+ identity,
850
+ rows: detached
851
+ });
852
+ }
853
+ };
854
+ function detachJson(rows) {
855
+ let text;
856
+ try {
857
+ text = JSON.stringify(rows);
858
+ } catch (error) {
859
+ throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
860
+ }
861
+ if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
862
+ return JSON.parse(text);
863
+ }
864
+ function identityOf(header, inheritedEventCount) {
865
+ const cut = SessionLogOffset(inheritedEventCount);
866
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
867
+ return {
868
+ formatVersion: header.version,
869
+ createdAt: header.createdAt,
870
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
871
+ isSeeded: header.isSeeded,
872
+ inheritedEventCount: cut
873
+ };
874
+ }
875
+ function identityMatches(stored, expected) {
876
+ return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
877
+ }
878
+ function predecessorIdentityMatches(stored, expected) {
879
+ return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
880
+ }
881
+ function lifecycleIdentityMatches(stored, expected) {
882
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
883
+ }
884
+ //#endregion
885
+ //#region src/storage-takeover/storage-backend.ts
886
+ const WORKSPACE_UNIT = "workspace";
887
+ const WORKSPACE_TABLE = "workspaces";
888
+ var RdbStorageBackend = class {
889
+ repository;
890
+ kv = { open: (descriptor) => this.openUnit(descriptor) };
891
+ open = /* @__PURE__ */ new Map();
892
+ closed = false;
893
+ constructor(repository) {
894
+ this.repository = repository;
895
+ }
896
+ async openUnit(descriptor) {
897
+ if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
898
+ if (descriptor.name !== WORKSPACE_UNIT) throw new Error(`rdb storage backend serves only the '${WORKSPACE_UNIT}' domain (requested '${descriptor.name}')`);
899
+ if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) throw new Error(`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape (table '${WORKSPACE_TABLE}' plus a global slot)`);
900
+ if (descriptor.layout !== void 0 && descriptor.layout !== "single") throw new Error(`rdb storage backend serves only the 'single' layout (domain '${descriptor.name}' declares '${descriptor.layout}')`);
901
+ if (this.open.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
902
+ const unit = new WorkspaceKvUnit(this.repository, descriptor);
903
+ this.open.set(descriptor.name, unit);
904
+ try {
905
+ const stored = await this.repository.readUnitVersion(descriptor.name);
906
+ if (stored === void 0) await this.repository.insertUnitVersion(descriptor.name, descriptor.version);
907
+ else if (stored !== descriptor.version) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${stored} on the medium, incompatible with descriptor version ${descriptor.version}`);
908
+ } catch (error) {
909
+ this.open.delete(descriptor.name);
910
+ throw error;
911
+ }
912
+ unit.onClose(() => {
913
+ this.open.delete(descriptor.name);
914
+ });
915
+ return unit;
916
+ }
917
+ async close() {
918
+ this.closed = true;
919
+ const units = [...this.open.values()];
920
+ this.open.clear();
921
+ await Promise.all(units.map((unit) => unit.close()));
922
+ }
923
+ };
924
+ var WorkspaceKvUnit = class {
925
+ repository;
926
+ descriptor;
927
+ closed = false;
928
+ inflight = /* @__PURE__ */ new Set();
929
+ onClosed;
930
+ constructor(repository, descriptor) {
931
+ this.repository = repository;
932
+ this.descriptor = descriptor;
933
+ }
934
+ onClose(release) {
935
+ this.onClosed = release;
936
+ }
937
+ async loadAll() {
938
+ this.assertOpen();
939
+ const records = Object.create(null);
940
+ for (const { id, record } of await this.repository.listWorkspaces()) records[id] = record;
941
+ const state = await this.repository.readWorkspaceState();
942
+ return {
943
+ tables: { [WORKSPACE_TABLE]: records },
944
+ global: state
945
+ };
946
+ }
947
+ async putRecord(table, key, value) {
948
+ this.assertOpen();
949
+ this.assertTable(table);
950
+ await this.track(this.repository.putWorkspace(key, workspaceRecordOf(value)));
951
+ }
952
+ async deleteRecord(table, key) {
953
+ this.assertOpen();
954
+ this.assertTable(table);
955
+ await this.track(this.repository.deleteWorkspace(key));
956
+ }
957
+ async setGlobal(value) {
958
+ this.assertOpen();
959
+ await this.track(this.repository.writeWorkspaceState(workspaceStateOf(value)));
960
+ }
961
+ async close() {
962
+ if (this.closed) return;
963
+ this.closed = true;
964
+ while (this.inflight.size > 0) await Promise.allSettled(this.inflight);
965
+ this.onClosed?.();
966
+ }
967
+ async track(operation) {
968
+ const settled = operation.then(() => void 0, () => void 0);
969
+ this.inflight.add(settled);
970
+ try {
971
+ await operation;
972
+ } finally {
973
+ this.inflight.delete(settled);
974
+ }
975
+ }
976
+ assertOpen() {
977
+ if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
978
+ }
979
+ assertTable(table) {
980
+ if (table !== WORKSPACE_TABLE) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
981
+ }
982
+ };
983
+ function workspaceRecordOf(value) {
984
+ const record = value;
985
+ if (typeof record !== "object" || record === null || typeof record.path !== "string" || typeof record.title !== "string" || !Array.isArray(record.sessionIds) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string") throw new TypeError("workspace record does not match the stored shape");
986
+ return {
987
+ path: record.path,
988
+ title: record.title,
989
+ sessionIds: record.sessionIds.map((id) => id),
990
+ createdAt: record.createdAt,
991
+ updatedAt: record.updatedAt
992
+ };
993
+ }
994
+ function workspaceStateOf(value) {
995
+ const state = value;
996
+ if (typeof state !== "object" || state === null || typeof state.initialized !== "boolean" || !Array.isArray(state.workspaceIds) || !Array.isArray(state.archivedSessionIds)) throw new TypeError("workspace registry state does not match the stored shape");
997
+ return {
998
+ initialized: state.initialized,
999
+ workspaceIds: state.workspaceIds.map((id) => id),
1000
+ archivedSessionIds: state.archivedSessionIds.map((id) => id),
1001
+ ...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
1002
+ };
1003
+ }
1004
+ //#endregion
1005
+ //#region src/storage-takeover/index.ts
1006
+ function installStorageTakeover(ctx, options) {
1007
+ ctx.inject(["storage"], (storageCtx) => {
1008
+ const backend = new RdbStorageBackend(options.repository);
1009
+ storageCtx.effect(() => {
1010
+ const unregister = storageCtx.storage.backend.register("rdb", backend);
1011
+ return async () => {
1012
+ unregister();
1013
+ await backend.close();
1014
+ };
1015
+ }, "session-rdb.storageBackend");
1016
+ storageCtx.provide(storageBackendServiceKey("rdb"), backend);
1017
+ });
1018
+ ctx.plugin(SessionProjectionCacheRdb, {
1019
+ ...options.projectionCache,
1020
+ repository: options.repository,
1021
+ ready: options.ready
1022
+ });
1023
+ }
1024
+ //#endregion
1025
+ //#region src/index.ts
1026
+ const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
1027
+ const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
1028
+ var SessionDeletionError = class extends Error {
1029
+ code;
1030
+ constructor(message, code) {
1031
+ super(message);
1032
+ this.code = code;
1033
+ this.name = "SessionDeletionError";
1034
+ }
1035
+ };
1036
+ var RdbBackendTracker = class {
1037
+ name;
1038
+ writers = /* @__PURE__ */ new Map();
1039
+ pending = /* @__PURE__ */ new Map();
1040
+ openHandles = /* @__PURE__ */ new Set();
1041
+ counter = 0;
1042
+ constructor(name) {
1043
+ this.name = name;
1044
+ }
1045
+ registerCreated(header, inheritedEventCount) {
1046
+ if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
1047
+ this.writers.set(header.id, null);
1048
+ this.pending.set(header.id, {
1049
+ header,
1050
+ revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
1051
+ inheritedEventCount,
1052
+ cursor: 0,
1053
+ everAppended: false
1054
+ });
1055
+ }
1056
+ updatePending(id, cursor, everAppended) {
1057
+ const entry = this.pending.get(id);
1058
+ if (entry === void 0) return;
1059
+ this.pending.set(id, {
1060
+ ...entry,
1061
+ cursor,
1062
+ everAppended
1063
+ });
1064
+ }
1065
+ claimWrite(id) {
1066
+ if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
1067
+ this.writers.set(id, null);
1068
+ }
1069
+ releaseClaim(id) {
1070
+ this.writers.delete(id);
1071
+ }
1072
+ pendingOf(id) {
1073
+ return this.pending.get(id);
1074
+ }
1075
+ hasPending(id) {
1076
+ return this.pending.has(id);
1077
+ }
1078
+ pendingEntries() {
1079
+ return this.pending.entries();
1080
+ }
1081
+ materialized(id) {
1082
+ this.pending.delete(id);
1083
+ }
1084
+ adopt(handle) {
1085
+ this.openHandles.add(handle);
1086
+ if (handle.access === "write") this.writers.set(handle.id, handle);
1087
+ return handle;
1088
+ }
1089
+ release(handle, materialized) {
1090
+ this.openHandles.delete(handle);
1091
+ if (handle.access !== "write") return;
1092
+ this.writers.delete(handle.id);
1093
+ if (!materialized) this.pending.delete(handle.id);
1094
+ }
1095
+ writerOf(id) {
1096
+ const writer = this.writers.get(id);
1097
+ return writer === null ? void 0 : writer;
1098
+ }
1099
+ hasOpenHandle(id) {
1100
+ for (const handle of this.openHandles) if (handle.id === id) return true;
1101
+ return false;
1102
+ }
1103
+ async flushAll() {
1104
+ const errors = [];
1105
+ for (const writer of this.writers.values()) {
1106
+ if (writer === null) continue;
1107
+ try {
1108
+ await writer.drainLive();
1109
+ await writer.flush();
1110
+ } catch (error) {
1111
+ if (error instanceof SessionHandleClosedError) continue;
1112
+ errors.push(error);
1113
+ }
1114
+ }
1115
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
1116
+ }
1117
+ async closeAll() {
1118
+ const errors = [];
1119
+ for (const handle of this.openHandles) try {
1120
+ await handle.close();
1121
+ } catch (error) {
1122
+ errors.push(error);
1123
+ }
1124
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
1125
+ }
1126
+ };
1127
+ var RdbSessionHandle = class RdbSessionHandle {
1128
+ persistence;
1129
+ id;
1130
+ header;
1131
+ access;
1132
+ state;
1133
+ chain = Promise.resolve();
1134
+ closing;
1135
+ cursor;
1136
+ materialized;
1137
+ buffered = [];
1138
+ batchTimer;
1139
+ drainPaused = false;
1140
+ draining;
1141
+ tornTruncateTo;
1142
+ everAppended = false;
1143
+ constructor(persistence, id, header, access, state) {
1144
+ this.persistence = persistence;
1145
+ this.id = id;
1146
+ this.header = header;
1147
+ this.access = access;
1148
+ this.state = state;
1149
+ this.cursor = state.cursor;
1150
+ this.materialized = state.materialized;
1151
+ this.tornTruncateTo = state.tornTruncateTo;
1152
+ }
1153
+ get inheritedEventCount() {
1154
+ return this.state.inheritedEventCount;
1155
+ }
1156
+ get cursorValue() {
1157
+ return this.cursor;
1158
+ }
1159
+ async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
1160
+ this.assertOpen("read");
1161
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
1162
+ if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
1163
+ options?.signal?.throwIfAborted();
1164
+ const log = await this.persistence.readLog(this.id, {}, options?.signal);
1165
+ if (log === void 0) {
1166
+ if (this.persistence.tracker.hasPending(this.id)) return {
1167
+ eventState: "detached",
1168
+ events: []
1169
+ };
1170
+ throw new SessionPersistenceNotFoundError(this.id);
1171
+ }
1172
+ repairReadView(log.events);
1173
+ return {
1174
+ eventState: "detached",
1175
+ events: log.events.slice(offset, offset + length)
1176
+ };
1177
+ }
1178
+ async append(events, options) {
1179
+ this.assertOpen("append");
1180
+ const batch = materializeAppendBatch(events);
1181
+ return this.run("append", async () => {
1182
+ options?.signal?.throwIfAborted();
1183
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
1184
+ if (batch.length === 0) return;
1185
+ this.everAppended = true;
1186
+ assertContiguous(this.id, batch, this.cursor);
1187
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
1188
+ this.tornTruncateTo = void 0;
1189
+ this.cursor += batch.length;
1190
+ this.materialized = true;
1191
+ this.persistence.tracker.updatePending(this.id, this.cursor, true);
1192
+ });
1193
+ }
1194
+ async flush(options) {
1195
+ return this.run("flush", async () => {
1196
+ options?.signal?.throwIfAborted();
1197
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
1198
+ if (this.materialized) return;
1199
+ await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
1200
+ this.materialized = true;
1201
+ });
1202
+ }
1203
+ close() {
1204
+ return this.closing ??= (async () => {
1205
+ let drainFailure;
1206
+ for (;;) {
1207
+ try {
1208
+ await this.drainLive();
1209
+ } catch (error) {
1210
+ drainFailure = error;
1211
+ break;
1212
+ }
1213
+ await this.chain;
1214
+ if (this.buffered.length === 0) break;
1215
+ }
1216
+ await this.chain;
1217
+ const failures = [];
1218
+ if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
1219
+ this.persistence.tracker.release(this, this.materialized || this.everAppended);
1220
+ if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
1221
+ if (failures[0] !== void 0) throw failures[0];
1222
+ })();
1223
+ }
1224
+ [Symbol.asyncDispose]() {
1225
+ return this.close();
1226
+ }
1227
+ enqueueLive(event, reportBackgroundFailure) {
1228
+ this.buffered.push(structuredClone(event));
1229
+ if (this.batchTimer !== void 0 || this.drainPaused) return;
1230
+ this.batchTimer = setTimeout(() => {
1231
+ this.batchTimer = void 0;
1232
+ this.drainLive().catch(reportBackgroundFailure);
1233
+ }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
1234
+ }
1235
+ resetAfterRewind(cursor, inheritedEventCount) {
1236
+ this.cursor = cursor;
1237
+ if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
1238
+ }
1239
+ drainLive() {
1240
+ return this.draining ??= this.drainBuffered().finally(() => {
1241
+ this.draining = void 0;
1242
+ });
1243
+ }
1244
+ async drainBuffered() {
1245
+ if (this.batchTimer !== void 0) {
1246
+ clearTimeout(this.batchTimer);
1247
+ this.batchTimer = void 0;
1248
+ }
1249
+ this.drainPaused = false;
1250
+ while (this.buffered.length > 0) await this.enqueueChain(async () => {
1251
+ const batch = this.buffered.splice(0);
1252
+ try {
1253
+ const fresh = batch.filter((event) => event.seq >= this.cursor);
1254
+ if (fresh.length === 0) return;
1255
+ for (const [index, event] of fresh.entries()) if (event.seq !== this.cursor + index) throw new Error(`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`);
1256
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
1257
+ this.tornTruncateTo = void 0;
1258
+ this.cursor += fresh.length;
1259
+ this.materialized = true;
1260
+ } catch (error) {
1261
+ this.buffered = batch.concat(this.buffered);
1262
+ this.drainPaused = true;
1263
+ throw error;
1264
+ }
1265
+ });
1266
+ }
1267
+ enqueueChain(op) {
1268
+ const next = this.chain.then(op);
1269
+ this.chain = next.catch(() => {});
1270
+ return next;
1271
+ }
1272
+ async run(operation, op) {
1273
+ this.assertOpen(operation);
1274
+ return this.enqueueChain(async () => {
1275
+ this.assertOpen(operation);
1276
+ return op();
1277
+ });
1278
+ }
1279
+ assertOpen(operation) {
1280
+ if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
1281
+ }
1282
+ static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
1283
+ };
1284
+ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
1285
+ config;
1286
+ static inject = ["sessions", "settings"];
1287
+ static Config = z.union([z.object({
1288
+ type: z.const("sqlite"),
1289
+ path: z.string().required(),
1290
+ journalMode: z.union([
1291
+ "wal",
1292
+ "delete",
1293
+ "truncate",
1294
+ "persist"
1295
+ ]).default("wal"),
1296
+ busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS),
1297
+ projectionCache: z.object({
1298
+ writeEveryEvents: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
1299
+ writeIntervalMs: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
1300
+ }).default({
1301
+ writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1302
+ writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1303
+ })
1304
+ }), z.object({
1305
+ type: z.const("postgres"),
1306
+ connectionString: z.string().required(),
1307
+ schema: z.string().default("public"),
1308
+ projectionCache: z.object({
1309
+ writeEveryEvents: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
1310
+ writeIntervalMs: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
1311
+ }).default({
1312
+ writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1313
+ writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1314
+ })
1315
+ })]);
1316
+ static settingsNs = "session-rdb";
1317
+ name = "session-rdb";
1318
+ tracker = new RdbBackendTracker(this.name);
1319
+ backend;
1320
+ storeIdentity;
1321
+ ready;
1322
+ writeGuard = new WriteGuard();
1323
+ reuseEventIds = /* @__PURE__ */ new Map();
1324
+ liveBuffers = /* @__PURE__ */ new Map();
1325
+ liveReady = /* @__PURE__ */ new Map();
1326
+ constructor(ctx, config, injectedBackend) {
1327
+ let resolved = config;
1328
+ const settings = ctx.reflect.get("settings");
1329
+ if (settings !== void 0) {
1330
+ const scope = settings.register(SessionPersistenceRdb.settingsNs, SessionPersistenceRdb.Config, { base: config });
1331
+ resolved = scope.get();
1332
+ scope.watch(() => {
1333
+ ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
1334
+ });
1335
+ }
1336
+ super(ctx);
1337
+ this.config = config;
1338
+ this.config = resolved;
1339
+ this.backend = injectedBackend ?? createBackend(resolved);
1340
+ this.ready = this.init();
1341
+ this.installLiveRouting(ctx);
1342
+ new SessionBranchRdb(this.ctx);
1343
+ this.ctx.plugin(SessionQueryRdb, {});
1344
+ registerSessionImport(this.ctx, this);
1345
+ registerSessionDeletion(this.ctx, this);
1346
+ registerSessionExport(this.ctx, this);
1347
+ registerSessionGc(this.ctx, this);
1348
+ registerSessionUsage(this.ctx, this);
1349
+ installStorageTakeover(this.ctx, {
1350
+ repository: this.backend.storage,
1351
+ ready: this.ready,
1352
+ projectionCache: {
1353
+ writeEveryEvents: this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1354
+ writeIntervalMs: this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1355
+ }
1356
+ });
1357
+ }
1358
+ async init() {
1359
+ await this.backend.open();
1360
+ this.storeIdentity = this.backend.storeIdentity;
1361
+ }
1362
+ async create(header, options) {
1363
+ options?.signal?.throwIfAborted();
1364
+ const snapshot = materializeCreateHeader(header);
1365
+ if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
1366
+ const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
1367
+ if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
1368
+ await this.ready;
1369
+ options?.signal?.throwIfAborted();
1370
+ if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
1371
+ this.tracker.registerCreated(snapshot, inheritedEventCount);
1372
+ return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
1373
+ cursor: 0,
1374
+ materialized: false,
1375
+ inheritedEventCount
1376
+ }));
1377
+ }
1378
+ async open(id, access, options) {
1379
+ options?.signal?.throwIfAborted();
1380
+ await this.ready;
1381
+ options?.signal?.throwIfAborted();
1382
+ const pending = this.tracker.pendingOf(id);
1383
+ if (access === "read") {
1384
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
1385
+ cursor: 0,
1386
+ materialized: false,
1387
+ inheritedEventCount: pending.inheritedEventCount
1388
+ }));
1389
+ const log = await this.readLog(id, {}, options?.signal);
1390
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1391
+ repairReadView(log.events);
1392
+ validateStoredEvents(log.meta, log.events);
1393
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
1394
+ cursor: log.events.length,
1395
+ materialized: true,
1396
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1397
+ }));
1398
+ }
1399
+ this.tracker.claimWrite(id);
1400
+ try {
1401
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
1402
+ cursor: pending.cursor,
1403
+ materialized: false,
1404
+ inheritedEventCount: pending.inheritedEventCount
1405
+ }));
1406
+ const log = await this.readLog(id, {}, options?.signal);
1407
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1408
+ repairReadView(log.events);
1409
+ validateStoredEvents(log.meta, log.events);
1410
+ if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
1411
+ this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
1412
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
1413
+ cursor: log.events.length,
1414
+ materialized: true,
1415
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1416
+ ...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
1417
+ }));
1418
+ } catch (error) {
1419
+ this.tracker.releaseClaim(id);
1420
+ throw error;
1421
+ }
1422
+ }
1423
+ flush() {
1424
+ return this.tracker.flushAll();
1425
+ }
1426
+ async stat(id, options) {
1427
+ options?.signal?.throwIfAborted();
1428
+ await this.ready;
1429
+ options?.signal?.throwIfAborted();
1430
+ const pending = this.tracker.pendingOf(id);
1431
+ if (pending !== void 0) return {
1432
+ header: pending.header,
1433
+ revision: pending.revision
1434
+ };
1435
+ const row = await this.backend.getSession(id);
1436
+ if (row === void 0) return void 0;
1437
+ return {
1438
+ header: rowToMeta(row),
1439
+ revision: this.rowRevision(row)
1440
+ };
1441
+ }
1442
+ async list(options) {
1443
+ const signal = options?.signal;
1444
+ const snapshots = [];
1445
+ const listed = /* @__PURE__ */ new Set();
1446
+ for (const [id, pending] of this.tracker.pendingEntries()) {
1447
+ snapshots.push({
1448
+ header: pending.header,
1449
+ revision: pending.revision
1450
+ });
1451
+ listed.add(id);
1452
+ }
1453
+ signal?.throwIfAborted();
1454
+ await this.ready;
1455
+ signal?.throwIfAborted();
1456
+ const rows = await this.backend.listSessions();
1457
+ signal?.throwIfAborted();
1458
+ for (const row of rows) {
1459
+ if (listed.has(row.fSessionId)) continue;
1460
+ snapshots.push({
1461
+ header: rowToMeta(row),
1462
+ revision: this.rowRevision(row)
1463
+ });
1464
+ }
1465
+ return snapshots;
1466
+ }
1467
+ async deleteSession(id, signal) {
1468
+ signal?.throwIfAborted();
1469
+ await this.ready;
1470
+ signal?.throwIfAborted();
1471
+ const row = await this.backend.getSession(id);
1472
+ if (row === void 0) throw new SessionDeletionError(`session "${id}" not found`, "SESSION_NOT_FOUND");
1473
+ if (row.fArchivedAt === null) throw new SessionDeletionError(`session "${id}" is not archived; only archived sessions can be deleted`, "SESSION_NOT_ARCHIVED");
1474
+ if (this.tracker.hasPending(id) || this.tracker.hasOpenHandle(id)) throw new SessionDeletionError(`session "${id}" is live; stop it before deleting`, "SESSION_LIVE");
1475
+ await this.unarchiveBeforeDeletion(id);
1476
+ await this.backend.transaction(async (tx) => {
1477
+ await tx.deleteSession(id);
1478
+ });
1479
+ this.liveBuffers.delete(id);
1480
+ this.liveReady.delete(id);
1481
+ this.reuseEventIds.delete(id);
1482
+ }
1483
+ /** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
1484
+ async collectOrphans() {
1485
+ await this.ready;
1486
+ return this.backend.collectOrphans();
1487
+ }
1488
+ /** GC 通道:回收父已不存在的 subagent 会话(live 的跳过),返回删除的会话数。 */
1489
+ async collectOrphanSessions() {
1490
+ await this.ready;
1491
+ const deletable = (await this.backend.listOrphanSubagentSessions()).filter((id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id));
1492
+ if (deletable.length === 0) return 0;
1493
+ const deleted = await this.backend.transaction((tx) => tx.deleteSessions(deletable));
1494
+ for (const id of deletable) {
1495
+ this.liveBuffers.delete(id);
1496
+ this.liveReady.delete(id);
1497
+ this.reuseEventIds.delete(id);
1498
+ }
1499
+ return deleted;
1500
+ }
1501
+ /** 用量统计:SQL 聚合的按天 × 模型桶与按会话行(事件行去重、排除孤儿行)。 */
1502
+ async usageReport(sinceMs) {
1503
+ await this.ready;
1504
+ return this.backend.usageReport(sinceMs);
1505
+ }
1506
+ /** GC 通道:VACUUM;调用方需先停止运行中的写路径(见 `registerSessionGc`)。 */
1507
+ async vacuum() {
1508
+ await this.ready;
1509
+ await this.backend.vacuum();
1510
+ }
1511
+ async unarchiveBeforeDeletion(id) {
1512
+ const registry = this.ctx.get("workspaceRegistry");
1513
+ if (registry === void 0) return;
1514
+ if (!registry.archivedSessionIds.includes(id)) return;
1515
+ await registry.unarchiveSession(id);
1516
+ }
1517
+ async readRaw(id, signal) {
1518
+ signal?.throwIfAborted();
1519
+ await this.ready;
1520
+ signal?.throwIfAborted();
1521
+ const log = await this.readLog(id, {}, signal);
1522
+ if (log === void 0) return void 0;
1523
+ repairReadView(log.events);
1524
+ const events = balanceRewindPrefix(log.events, { keepOpenTail: true });
1525
+ const inheritedEventCount = Math.min(log.inheritedEventCount, events.length);
1526
+ return {
1527
+ meta: log.meta,
1528
+ inheritedEventCount,
1529
+ filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
1530
+ content: toJsonlArtifact(log.meta, inheritedEventCount, events)
1531
+ };
1532
+ }
1533
+ async materializeEmpty(meta, inheritedEventCount) {
1534
+ await this.ready;
1535
+ await this.backend.transaction(async (tx) => {
1536
+ await tx.upsertSession({
1537
+ meta,
1538
+ inheritedEventCount
1539
+ }, randomUUID());
1540
+ await tx.bumpRevision(meta.id);
1541
+ });
1542
+ this.tracker.materialized(meta.id);
1543
+ this.writeGuard.confirmHead(meta.id, -1);
1544
+ }
1545
+ async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
1546
+ await this.ready;
1547
+ if (events.length === 0) return false;
1548
+ validateStoredEvents(meta, [...events]);
1549
+ const reuse = this.reuseEventIds.get(meta.id);
1550
+ if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
1551
+ let confirmedHead = -1;
1552
+ await this.backend.transaction(async (tx) => {
1553
+ if (tornTruncateTo !== void 0) {
1554
+ await tx.deleteBridgeTail(meta.id, tornTruncateTo);
1555
+ const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
1556
+ if (prev === void 0) await tx.updateHead(meta.id, "", -1);
1557
+ else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
1558
+ }
1559
+ await tx.upsertSession({
1560
+ meta,
1561
+ inheritedEventCount
1562
+ }, randomUUID());
1563
+ const head = await tx.getHead(meta.id);
1564
+ this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
1565
+ const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
1566
+ parentId: head.fHeadEventId,
1567
+ nextSeq: head.fHeadSequence + 1
1568
+ }, reuse);
1569
+ await tx.updateHead(meta.id, headEventId, headSequence);
1570
+ await tx.bumpRevision(meta.id);
1571
+ confirmedHead = headSequence;
1572
+ });
1573
+ this.writeGuard.confirmHead(meta.id, confirmedHead);
1574
+ this.tracker.materialized(meta.id);
1575
+ return true;
1576
+ }
1577
+ async readLog(id, options = {}, signal) {
1578
+ signal?.throwIfAborted();
1579
+ await this.ready;
1580
+ signal?.throwIfAborted();
1581
+ const row = await this.backend.getSession(id);
1582
+ if (row === void 0) return void 0;
1583
+ const meta = rowToMeta(row);
1584
+ const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
1585
+ signal?.throwIfAborted();
1586
+ if (isLegacyVersion(row.fVersion)) try {
1587
+ const converted = convertLegacyRows(row, eventRows);
1588
+ return {
1589
+ meta: converted.meta,
1590
+ inheritedEventCount: converted.inheritedEventCount,
1591
+ events: converted.events,
1592
+ incarnation: row.fIncarnation,
1593
+ revision: row.fRevision,
1594
+ storedCount: eventRows.length,
1595
+ migrated: true
1596
+ };
1597
+ } catch (error) {
1598
+ this.ctx.logger.warn(`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`);
1599
+ const adopted = adoptLegacyRows(row, eventRows);
1600
+ return {
1601
+ meta: adopted.meta,
1602
+ inheritedEventCount: adopted.inheritedEventCount,
1603
+ events: adopted.events,
1604
+ incarnation: row.fIncarnation,
1605
+ revision: row.fRevision,
1606
+ storedCount: eventRows.length,
1607
+ migrated: false,
1608
+ ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
1609
+ };
1610
+ }
1611
+ const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
1612
+ return {
1613
+ meta,
1614
+ inheritedEventCount: row.fSeedLength ?? 0,
1615
+ events: preserved,
1616
+ incarnation: row.fIncarnation,
1617
+ revision: row.fRevision,
1618
+ storedCount: eventRows.length,
1619
+ migrated: false,
1620
+ ...tornFrom !== void 0 ? { tornFrom } : {}
1621
+ };
1622
+ }
1623
+ async rewriteMigratedLog(id, log) {
1624
+ await this.backend.transaction(async (tx) => {
1625
+ await tx.deleteBridgeTail(id, 0);
1626
+ await tx.upsertSession({
1627
+ meta: log.meta,
1628
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1629
+ }, randomUUID());
1630
+ const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
1631
+ parentId: "",
1632
+ nextSeq: 0
1633
+ });
1634
+ await tx.updateHead(id, headEventId, headSequence);
1635
+ await tx.bumpRevision(id);
1636
+ });
1637
+ }
1638
+ async listSnapshots(signal) {
1639
+ signal?.throwIfAborted();
1640
+ await this.ready;
1641
+ signal?.throwIfAborted();
1642
+ const snapshots = [];
1643
+ const listed = /* @__PURE__ */ new Set();
1644
+ for (const [id, pending] of this.tracker.pendingEntries()) {
1645
+ snapshots.push({
1646
+ header: pending.header,
1647
+ revision: pending.revision,
1648
+ inheritedEventCount: pending.inheritedEventCount
1649
+ });
1650
+ listed.add(id);
1651
+ }
1652
+ const rows = await this.backend.listSessions();
1653
+ signal?.throwIfAborted();
1654
+ for (const row of rows) {
1655
+ if (listed.has(row.fSessionId)) continue;
1656
+ snapshots.push({
1657
+ header: rowToMeta(row),
1658
+ revision: this.rowRevision(row),
1659
+ inheritedEventCount: row.fSeedLength ?? 0
1660
+ });
1661
+ }
1662
+ return snapshots;
1663
+ }
1664
+ async readStoredRevision(id, signal) {
1665
+ signal?.throwIfAborted();
1666
+ await this.ready;
1667
+ signal?.throwIfAborted();
1668
+ const row = await this.backend.getSession(id);
1669
+ if (row === void 0) return void 0;
1670
+ return this.rowRevision(row);
1671
+ }
1672
+ async createAndAppend(header, events, inheritedEventCount) {
1673
+ const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
1674
+ try {
1675
+ if (events.length > 0) await handle.append(events);
1676
+ } finally {
1677
+ await handle.close();
1678
+ }
1679
+ }
1680
+ async load(id, signal) {
1681
+ const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
1682
+ try {
1683
+ const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
1684
+ const row = await this.backend.getSession(id);
1685
+ if (row === void 0) throw new SessionPersistenceNotFoundError(id);
1686
+ if (isLegacyVersion(row.fVersion)) return {
1687
+ meta: handle.header,
1688
+ inheritedEventCount: handle.inheritedEventCount,
1689
+ events
1690
+ };
1691
+ return {
1692
+ meta: rowToMeta(row),
1693
+ inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
1694
+ events
1695
+ };
1696
+ } finally {
1697
+ await handle.close();
1698
+ }
1699
+ }
1700
+ async append(id, events) {
1701
+ const handle = await this.open(id, "write");
1702
+ try {
1703
+ await handle.append(events);
1704
+ } finally {
1705
+ await handle.close();
1706
+ }
1707
+ }
1708
+ async readFrom(id, fromSeq, signal) {
1709
+ const log = await this.readLog(id, { fromSeq }, signal);
1710
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1711
+ return {
1712
+ meta: log.meta,
1713
+ inheritedEventCount: log.inheritedEventCount,
1714
+ events: log.events
1715
+ };
1716
+ }
1717
+ async close() {
1718
+ await this.ready;
1719
+ await this.backend.close();
1720
+ }
1721
+ registerReuseEventIds(childId, map) {
1722
+ this.reuseEventIds.set(childId, new Map(map));
1723
+ }
1724
+ internals() {
1725
+ return {
1726
+ backend: this.backend,
1727
+ writeGuard: this.writeGuard,
1728
+ create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
1729
+ append: (id, events) => this.append(id, events),
1730
+ load: (id) => this.load(id),
1731
+ inspect: (id, signal) => this.load(id, signal),
1732
+ readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
1733
+ listSnapshots: (signal) => this.listSnapshots(signal),
1734
+ readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
1735
+ registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
1736
+ };
1737
+ }
1738
+ rowRevision(row) {
1739
+ return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
1740
+ }
1741
+ installLiveRouting(ctx) {
1742
+ ctx.on("session/created", (session) => {
1743
+ this.liveBuffers.set(session.id, []);
1744
+ const ready = this.ensureLiveHandle(session);
1745
+ this.liveReady.set(session.id, ready);
1746
+ ready.catch((error) => {
1747
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1748
+ });
1749
+ });
1750
+ for (const session of ctx.sessions.list()) {
1751
+ this.liveBuffers.set(session.id, []);
1752
+ const ready = this.ensureLiveHandle(session);
1753
+ this.liveReady.set(session.id, ready);
1754
+ ready.catch((error) => {
1755
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1756
+ });
1757
+ }
1758
+ ctx.on("session/event", (session, event) => {
1759
+ const handle = this.tracker.writerOf(session.id);
1760
+ if (handle !== void 0) {
1761
+ handle.enqueueLive(event, (error) => {
1762
+ ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
1763
+ });
1764
+ return;
1765
+ }
1766
+ this.liveBuffers.get(session.id)?.push(structuredClone(event));
1767
+ });
1768
+ ctx.on("session/flush", (session) => {
1769
+ const handle = this.tracker.writerOf(session.id);
1770
+ if (handle === void 0) {
1771
+ const ready = this.liveReady.get(session.id);
1772
+ if (ready === void 0) return void 0;
1773
+ return ready.then(() => {
1774
+ const settled = this.tracker.writerOf(session.id);
1775
+ if (settled === void 0) return void 0;
1776
+ return settled.drainLive().then(() => settled.flush());
1777
+ });
1778
+ }
1779
+ return handle.drainLive().then(() => handle.flush());
1780
+ });
1781
+ ctx.on("session/disposed", (session) => {
1782
+ const ready = this.liveReady.get(session.id);
1783
+ this.liveBuffers.delete(session.id);
1784
+ this.liveReady.delete(session.id);
1785
+ const closeHandle = () => {
1786
+ const handle = this.tracker.writerOf(session.id);
1787
+ if (handle === void 0) return;
1788
+ handle.close().catch((error) => {
1789
+ ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
1790
+ });
1791
+ };
1792
+ if (ready === void 0) {
1793
+ closeHandle();
1794
+ return;
1795
+ }
1796
+ ready.then(closeHandle, closeHandle);
1797
+ });
1798
+ ctx.effect(() => async () => {
1799
+ await Promise.allSettled(this.liveReady.values());
1800
+ await this.tracker.closeAll();
1801
+ await this.close();
1802
+ }, `${this.name} open handles`);
1803
+ }
1804
+ async ensureLiveHandle(session) {
1805
+ const id = session.header.id;
1806
+ if (this.tracker.writerOf(id) !== void 0) return;
1807
+ await this.ready;
1808
+ const stored = await this.readLog(id, {});
1809
+ let handle;
1810
+ if (stored === void 0) {
1811
+ handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
1812
+ const seed = session.snapshotEvents();
1813
+ if (seed.length > 0) await handle.append(seed);
1814
+ } else {
1815
+ if (stored.meta.cwd !== session.header.cwd) throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
1816
+ if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
1817
+ assertVersion(stored.meta);
1818
+ repairReadView(stored.events);
1819
+ const seed = session.snapshotEvents();
1820
+ if (!seedCoversPrefix(seed, stored.events)) throw new Error(`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`);
1821
+ handle = await this.open(id, "write");
1822
+ const suffix = seed.slice(stored.events.length);
1823
+ if (suffix.length > 0) await handle.append(suffix);
1824
+ }
1825
+ const buffered = this.liveBuffers.get(id);
1826
+ if (buffered !== void 0 && buffered.length > 0) {
1827
+ this.liveBuffers.set(id, []);
1828
+ for (const event of buffered) handle.enqueueLive(event, () => {});
1829
+ }
1830
+ }
1831
+ };
1832
+ function seedCoversPrefix(seed, prefix) {
1833
+ return prefix.length <= seed.length && prefix.every((event, index) => {
1834
+ const seedEvent = seed[index];
1835
+ return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
1836
+ });
1837
+ }
1838
+ function createBackend(config) {
1839
+ if (config.type === "sqlite") return new SqliteBackend({
1840
+ path: config.path,
1841
+ journalMode: config.journalMode ?? "wal",
1842
+ busyTimeout: config.busyTimeout ?? 5e3
1843
+ });
1844
+ const pool = new Pool({ connectionString: config.connectionString });
1845
+ pool.on("error", () => {});
1846
+ return new PostgresBackend(drizzle({ client: pool }), {
1847
+ identityBase: [
1848
+ "postgres",
1849
+ pool.options.host ?? "localhost",
1850
+ String(pool.options.port ?? 5432),
1851
+ pool.options.database ?? "",
1852
+ config.schema ?? "public"
1853
+ ].join(":"),
1854
+ schema: config.schema ?? "public",
1855
+ close: () => pool.end()
1856
+ });
1857
+ }
1858
+ async function appendEventTail(tx, meta, events, anchor, reuse) {
1859
+ let parentId = anchor.parentId;
1860
+ let nextSeq = anchor.nextSeq;
1861
+ const eventRows = [];
1862
+ const bridgeRows = [];
1863
+ for (const event of events) {
1864
+ const reusedId = reuse?.get(event.seq);
1865
+ const eventId = reusedId ?? randomUUID();
1866
+ if (reusedId === void 0) {
1867
+ const { kind, role, name, actionId } = eventDimensions(event);
1868
+ const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
1869
+ eventRows.push({
1870
+ fEventId: eventId,
1871
+ fParentId: parentId,
1872
+ fType: event.type,
1873
+ fKind: kind,
1874
+ fRole: role,
1875
+ fName: name,
1876
+ fActionId: actionId,
1877
+ fEncoding: EVENT_ENCODING,
1878
+ fData: JSON.stringify({
1879
+ ...envelope,
1880
+ data
1881
+ }),
1882
+ fCreatedAt: event.time
1883
+ });
1884
+ }
1885
+ const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
1886
+ bridgeRows.push({
1887
+ fSessionId: meta.id,
1888
+ fEventId: eventId,
1889
+ fSequence: nextSeq,
1890
+ fSurfaceOp: surfaceOp
1891
+ });
1892
+ parentId = eventId;
1893
+ nextSeq++;
1894
+ }
1895
+ if (eventRows.length > 0) await tx.insertEvents(eventRows);
1896
+ await tx.insertBridges(bridgeRows);
1897
+ if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
1898
+ return {
1899
+ headEventId: parentId,
1900
+ headSequence: nextSeq - 1
1901
+ };
1902
+ }
1903
+ //#endregion
1904
+ export { registerSessionDeletion as i, SessionPersistenceRdb as n, SESSION_DELETE_PATH as r, SessionDeletionError as t };