@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-alpha.2

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 (142) hide show
  1. package/README.md +18 -3
  2. package/dist/artifact.d.mts +118 -2
  3. package/dist/artifact.mjs +5 -3
  4. package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
  5. package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
  6. package/dist/branch-CG1PUlO6.mjs +833 -0
  7. package/dist/deletion-SSX7IqU8.mjs +35 -0
  8. package/dist/deletion.d.mts +1 -1
  9. package/dist/deletion.mjs +2 -2
  10. package/dist/export-CfJQjQfr.mjs +123 -0
  11. package/dist/export.d.mts +1 -1
  12. package/dist/export.mjs +31 -55
  13. package/dist/import.d.mts +1 -1
  14. package/dist/import.mjs +31 -76
  15. package/dist/index-CUD2ZpTA.d.mts +71 -0
  16. package/dist/index.d.mts +20 -2
  17. package/dist/index.mjs +1561 -4
  18. package/dist/open-YoyD_kah.mjs +561 -0
  19. package/dist/organize.d.mts +9 -0
  20. package/dist/organize.mjs +30 -0
  21. package/dist/route-DpfY0zq-.mjs +114 -0
  22. package/dist/storage.d.mts +41 -48
  23. package/dist/storage.mjs +3 -1
  24. package/dist/testing.d.mts +9 -1
  25. package/dist/testing.mjs +25 -4
  26. package/dist/usage-row-BQQM-_t6.mjs +36 -0
  27. package/dist/usage.d.mts +1 -1
  28. package/dist/usage.mjs +7 -40
  29. package/dist/write-guard-CYrY58GM.mjs +174 -0
  30. package/package.json +10 -14
  31. package/src/artifact.ts +4 -1
  32. package/src/backend-rdb.ts +331 -0
  33. package/src/backend.ts +32 -15
  34. package/src/{branch.ts → branch/index.ts} +109 -110
  35. package/src/branch/port.ts +62 -0
  36. package/src/branch/semantics.ts +69 -0
  37. package/src/db/open.ts +180 -0
  38. package/src/db/stats.ts +449 -0
  39. package/src/deletion-error.ts +17 -0
  40. package/src/deletion.ts +19 -72
  41. package/src/entities/index.ts +16 -1
  42. package/src/entities/v3/event-usage.ts +3 -3
  43. package/src/entities/v3/events.ts +3 -3
  44. package/src/entities/v3/index.ts +4 -16
  45. package/src/entities/v3/persistence-state.ts +3 -3
  46. package/src/entities/v3/schema-meta.ts +3 -3
  47. package/src/entities/v3/session-counts.ts +3 -3
  48. package/src/entities/v3/session-events.ts +3 -3
  49. package/src/entities/v3/session-projcache-rows.ts +3 -3
  50. package/src/entities/v3/session-usage.ts +3 -3
  51. package/src/entities/v3/sessions.ts +3 -3
  52. package/src/entities/v3/storage-records.ts +17 -0
  53. package/src/entities/v3/storage-units.ts +3 -3
  54. package/src/{schema.ts → event-shape.ts} +0 -46
  55. package/src/export.ts +32 -78
  56. package/src/handles.ts +349 -0
  57. package/src/http/route.ts +178 -0
  58. package/src/import-storages.ts +5 -2
  59. package/src/import.ts +63 -125
  60. package/src/index.ts +15 -1666
  61. package/src/legacy.ts +3 -1
  62. package/src/live-routing.ts +205 -0
  63. package/src/log/export.ts +84 -0
  64. package/src/{log.ts → log/repair.ts} +1 -268
  65. package/src/log/rows.ts +107 -0
  66. package/src/log/usage-row.ts +64 -0
  67. package/src/migrations.ts +196 -0
  68. package/src/organize.ts +38 -0
  69. package/src/read-path.ts +190 -0
  70. package/src/routes.ts +2 -1
  71. package/src/rows.ts +25 -77
  72. package/src/session-ops.ts +444 -0
  73. package/src/session-store.ts +675 -0
  74. package/src/stats-fold.ts +44 -0
  75. package/src/storage-takeover/index.ts +2 -5
  76. package/src/storage-takeover/legacy-domain-migration.ts +118 -0
  77. package/src/storage-takeover/projection-cache.ts +122 -71
  78. package/src/storage-takeover/read-model.ts +189 -0
  79. package/src/storage-takeover/repository.ts +236 -442
  80. package/src/storage-takeover/storage-backend.ts +44 -159
  81. package/src/storage-takeover/types.ts +45 -19
  82. package/src/storage-takeover/workspace-documents.ts +65 -0
  83. package/src/storage.ts +4 -2
  84. package/src/testing.ts +21 -1
  85. package/src/usage.ts +11 -65
  86. package/src/version.ts +13 -0
  87. package/src/write-path.ts +286 -0
  88. package/dist/branch-CoPCRKer.mjs +0 -401
  89. package/dist/deletion-CWl89TIR.mjs +0 -2249
  90. package/dist/gc.d.mts +0 -4
  91. package/dist/gc.mjs +0 -48
  92. package/dist/log-CcWy46VE.mjs +0 -558
  93. package/dist/routes-hZjTcX_R.mjs +0 -9
  94. package/dist/sqlite-BNRd1aqR.mjs +0 -1778
  95. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
  96. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
  97. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
  98. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
  99. package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
  100. package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
  101. package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
  102. package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
  103. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
  104. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
  105. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
  106. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
  107. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
  108. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
  109. package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
  110. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
  111. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
  112. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
  113. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
  114. package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
  115. package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
  116. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
  117. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
  118. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
  119. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
  120. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
  121. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
  122. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
  123. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
  124. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
  125. package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
  126. package/src/adapters/index.ts +0 -12
  127. package/src/adapters/to-postgres.ts +0 -93
  128. package/src/adapters/to-sqlite.ts +0 -83
  129. package/src/adapters/types.ts +0 -44
  130. package/src/drizzle/postgres-v2.ts +0 -10
  131. package/src/drizzle/postgres-v3.ts +0 -18
  132. package/src/drizzle/sqlite-v2.ts +0 -9
  133. package/src/drizzle/sqlite-v3.ts +0 -18
  134. package/src/entities/v2/index.ts +0 -20
  135. package/src/entities/v2/session-events.ts +0 -10
  136. package/src/entities/v3/schedule-tasks.ts +0 -16
  137. package/src/entities/v3/workspace-sessions.ts +0 -19
  138. package/src/entities/v3/workspace-state.ts +0 -12
  139. package/src/entities/v3/workspaces.ts +0 -14
  140. package/src/gc.ts +0 -77
  141. package/src/postgres.ts +0 -1015
  142. package/src/sqlite.ts +0 -1096
package/dist/index.mjs CHANGED
@@ -1,5 +1,1562 @@
1
- import { a as SESSION_ROWS_PATH } from "./routes-hZjTcX_R.mjs";
2
- import { i as locateTurnEnd, n as SessionBranchRdbProvider, r as liveEventsOf, t as SessionBranchRdb } from "./branch-CoPCRKer.mjs";
3
- import { o as SCHEMA_VERSION } from "./sqlite-BNRd1aqR.mjs";
4
- import { n as SessionDeletionError, r as SessionPersistenceRdb } from "./deletion-CWl89TIR.mjs";
1
+ import { a as rowToMeta, r as toJsonlArtifact, t as scanRows } from "./export-CfJQjQfr.mjs";
2
+ import { C as sealOwnEvents, b as repairReadView, c as convertLegacyRows, l as isLegacyVersion, n as SessionBranchRdbProvider, o as locateTurnEnd, p as needsShapeAdoption, r as liveEventsOf, s as adoptLegacyRows, t as SessionBranchRdb } from "./branch-CG1PUlO6.mjs";
3
+ import { c as SESSION_ROWS_PATH, n as mountJsonRoute } from "./route-DpfY0zq-.mjs";
4
+ import { COUNTED_EVENT_TYPES, addActivityCount, localDayKey, registerSessionUsage } from "./usage.mjs";
5
+ import { t as usageRowOf } from "./usage-row-BQQM-_t6.mjs";
6
+ import { registerSessionImport } from "./import.mjs";
7
+ import { n as SessionDeletionError, t as registerSessionDeletion } from "./deletion-SSX7IqU8.mjs";
8
+ import { registerSessionExport } from "./export.mjs";
9
+ import { registerSessionOrganize } from "./organize.mjs";
10
+ import { d as EVENT_ENCODING, f as SCHEMA_VERSION, u as DEFAULT_BUSY_TIMEOUT_MS } from "./backend-rdb-BDCntVDy.mjs";
11
+ import { n as eventDimensions, t as WriteGuard } from "./write-guard-CYrY58GM.mjs";
12
+ import { t as openRdbBackend } from "./open-YoyD_kah.mjs";
13
+ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
14
+ import { randomUUID } from "node:crypto";
15
+ import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
16
+ import { balanceRewindPrefix } from "@morlay/session-branch";
17
+ import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
18
+ import { Service } from "@deepseek-ai/cordis";
19
+ import z from "@deepseek-ai/schemastery";
20
+ import { SessionQueryEngine, SessionQueryError } from "@deepseek-ai/dsh-session-query";
21
+ import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
22
+ // src/handles.ts
23
+ var RdbBackendTracker = class {
24
+ name;
25
+ writers = /* @__PURE__ */ new Map();
26
+ pending = /* @__PURE__ */ new Map();
27
+ openHandles = /* @__PURE__ */ new Set();
28
+ counter = 0;
29
+ constructor(name) {
30
+ this.name = name;
31
+ }
32
+ registerCreated(header, inheritedEventCount) {
33
+ if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
34
+ this.writers.set(header.id, null);
35
+ this.pending.set(header.id, {
36
+ header,
37
+ revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
38
+ inheritedEventCount,
39
+ cursor: 0,
40
+ everAppended: false
41
+ });
42
+ }
43
+ updatePending(id, cursor, everAppended) {
44
+ const entry = this.pending.get(id);
45
+ if (entry === void 0) return;
46
+ this.pending.set(id, {
47
+ ...entry,
48
+ cursor,
49
+ everAppended
50
+ });
51
+ }
52
+ claimWrite(id) {
53
+ if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
54
+ this.writers.set(id, null);
55
+ }
56
+ releaseClaim(id) {
57
+ this.writers.delete(id);
58
+ }
59
+ pendingOf(id) {
60
+ return this.pending.get(id);
61
+ }
62
+ hasPending(id) {
63
+ return this.pending.has(id);
64
+ }
65
+ pendingEntries() {
66
+ return this.pending.entries();
67
+ }
68
+ materialized(id) {
69
+ this.pending.delete(id);
70
+ }
71
+ adopt(handle) {
72
+ this.openHandles.add(handle);
73
+ if (handle.access === "write") this.writers.set(handle.id, handle);
74
+ return handle;
75
+ }
76
+ release(handle, materialized) {
77
+ this.openHandles.delete(handle);
78
+ if (handle.access !== "write") return;
79
+ this.writers.delete(handle.id);
80
+ if (!materialized) this.pending.delete(handle.id);
81
+ }
82
+ writerOf(id) {
83
+ const writer = this.writers.get(id);
84
+ return writer === null ? void 0 : writer;
85
+ }
86
+ hasOpenHandle(id) {
87
+ for (const handle of this.openHandles) if (handle.id === id) return true;
88
+ return false;
89
+ }
90
+ async flushAll() {
91
+ const errors = [];
92
+ for (const writer of this.writers.values()) {
93
+ if (writer === null) continue;
94
+ try {
95
+ await writer.drainLive();
96
+ await writer.flush();
97
+ } catch (error) {
98
+ if (error instanceof SessionHandleClosedError) continue;
99
+ errors.push(error);
100
+ }
101
+ }
102
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
103
+ }
104
+ async closeAll() {
105
+ const errors = [];
106
+ for (const handle of this.openHandles) try {
107
+ await handle.close();
108
+ } catch (error) {
109
+ errors.push(error);
110
+ }
111
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
112
+ }
113
+ };
114
+ var RdbSessionHandle = class RdbSessionHandle {
115
+ persistence;
116
+ id;
117
+ header;
118
+ access;
119
+ state;
120
+ chain = Promise.resolve();
121
+ closing;
122
+ cursor;
123
+ materialized;
124
+ buffered = [];
125
+ batchTimer;
126
+ drainPaused = false;
127
+ draining;
128
+ tornTruncateTo;
129
+ everAppended = false;
130
+ constructor(persistence, id, header, access, state) {
131
+ this.persistence = persistence;
132
+ this.id = id;
133
+ this.header = header;
134
+ this.access = access;
135
+ this.state = state;
136
+ this.cursor = state.cursor;
137
+ this.materialized = state.materialized;
138
+ this.tornTruncateTo = state.tornTruncateTo;
139
+ }
140
+ get inheritedEventCount() {
141
+ return this.state.inheritedEventCount;
142
+ }
143
+ get cursorValue() {
144
+ return this.cursor;
145
+ }
146
+ async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
147
+ this.assertOpen("read");
148
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
149
+ if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
150
+ options?.signal?.throwIfAborted();
151
+ const log = await this.persistence.readLog(this.id, {}, options?.signal);
152
+ if (log === void 0) {
153
+ if (this.persistence.tracker.hasPending(this.id)) return {
154
+ eventState: "detached",
155
+ events: []
156
+ };
157
+ throw new SessionPersistenceNotFoundError(this.id);
158
+ }
159
+ repairReadView(log.events);
160
+ return {
161
+ eventState: "detached",
162
+ events: log.events.slice(offset, offset + length)
163
+ };
164
+ }
165
+ async append(events, options) {
166
+ this.assertOpen("append");
167
+ const batch = materializeAppendBatch(events);
168
+ return this.run("append", async () => {
169
+ options?.signal?.throwIfAborted();
170
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
171
+ if (batch.length === 0) return;
172
+ this.everAppended = true;
173
+ assertContiguous(this.id, batch, this.cursor);
174
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
175
+ this.tornTruncateTo = void 0;
176
+ this.cursor += batch.length;
177
+ this.materialized = true;
178
+ this.persistence.tracker.updatePending(this.id, this.cursor, true);
179
+ });
180
+ }
181
+ async flush(options) {
182
+ return this.run("flush", async () => {
183
+ options?.signal?.throwIfAborted();
184
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
185
+ if (this.materialized) return;
186
+ await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
187
+ this.materialized = true;
188
+ });
189
+ }
190
+ close() {
191
+ return this.closing ??= (async () => {
192
+ let drainFailure;
193
+ for (;;) {
194
+ try {
195
+ await this.drainLive();
196
+ } catch (error) {
197
+ drainFailure = error;
198
+ break;
199
+ }
200
+ await this.chain;
201
+ if (this.buffered.length === 0) break;
202
+ }
203
+ await this.chain;
204
+ const failures = [];
205
+ if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
206
+ this.persistence.tracker.release(this, this.materialized || this.everAppended);
207
+ if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
208
+ if (failures[0] !== void 0) throw failures[0];
209
+ })();
210
+ }
211
+ [Symbol.asyncDispose]() {
212
+ return this.close();
213
+ }
214
+ enqueueLive(event, reportBackgroundFailure) {
215
+ this.buffered.push(structuredClone(event));
216
+ if (this.batchTimer !== void 0 || this.drainPaused) return;
217
+ this.batchTimer = setTimeout(() => {
218
+ this.batchTimer = void 0;
219
+ this.drainLive().catch(reportBackgroundFailure);
220
+ }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
221
+ }
222
+ resetAfterRewind(cursor, inheritedEventCount) {
223
+ this.cursor = cursor;
224
+ if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
225
+ }
226
+ drainLive() {
227
+ return this.draining ??= this.drainBuffered().finally(() => {
228
+ this.draining = void 0;
229
+ });
230
+ }
231
+ async drainBuffered() {
232
+ if (this.batchTimer !== void 0) {
233
+ clearTimeout(this.batchTimer);
234
+ this.batchTimer = void 0;
235
+ }
236
+ this.drainPaused = false;
237
+ while (this.buffered.length > 0) await this.enqueueChain(async () => {
238
+ const batch = this.buffered.splice(0);
239
+ try {
240
+ const fresh = batch.filter((event) => event.seq >= this.cursor);
241
+ if (fresh.length === 0) return;
242
+ 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}`);
243
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
244
+ this.tornTruncateTo = void 0;
245
+ this.cursor += fresh.length;
246
+ this.materialized = true;
247
+ } catch (error) {
248
+ this.buffered = batch.concat(this.buffered);
249
+ this.drainPaused = true;
250
+ throw error;
251
+ }
252
+ });
253
+ }
254
+ enqueueChain(op) {
255
+ const next = this.chain.then(op);
256
+ this.chain = next.catch(() => {});
257
+ return next;
258
+ }
259
+ async run(operation, op) {
260
+ this.assertOpen(operation);
261
+ return this.enqueueChain(async () => {
262
+ this.assertOpen(operation);
263
+ return op();
264
+ });
265
+ }
266
+ assertOpen(operation) {
267
+ if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
268
+ }
269
+ static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
270
+ };
271
+ // src/live-routing.ts
272
+ var LiveRouting = class {
273
+ ctx;
274
+ persistence;
275
+ buffers = /* @__PURE__ */ new Map();
276
+ ready = /* @__PURE__ */ new Map();
277
+ failures = /* @__PURE__ */ new Map();
278
+ dropWarned = /* @__PURE__ */ new Set();
279
+ constructor(ctx, persistence) {
280
+ this.ctx = ctx;
281
+ this.persistence = persistence;
282
+ }
283
+ /** 丢掉一个会话的所有 live 记账(删除会话、孤儿会话回收时)。 */
284
+ forget(id) {
285
+ this.buffers.delete(id);
286
+ this.ready.delete(id);
287
+ this.failures.delete(id);
288
+ this.dropWarned.delete(id);
289
+ }
290
+ install() {
291
+ const ctx = this.ctx;
292
+ ctx.on("session/created", (session) => {
293
+ this.buffers.set(session.id, []);
294
+ const ready = this.ensureHandle(session);
295
+ this.ready.set(session.id, ready);
296
+ this.watchReady(session, ready);
297
+ });
298
+ for (const session of ctx.sessions.list()) {
299
+ this.buffers.set(session.id, []);
300
+ const ready = this.ensureHandle(session);
301
+ this.ready.set(session.id, ready);
302
+ this.watchReady(session, ready);
303
+ }
304
+ ctx.on("session/event", (session, event) => {
305
+ const handle = this.persistence.tracker.writerOf(session.id);
306
+ if (handle !== void 0) {
307
+ handle.enqueueLive(event, (error) => {
308
+ ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
309
+ });
310
+ return;
311
+ }
312
+ if (this.failures.has(session.id)) {
313
+ if (!this.dropWarned.has(session.id)) {
314
+ this.dropWarned.add(session.id);
315
+ ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
316
+ }
317
+ return;
318
+ }
319
+ this.buffers.get(session.id)?.push(structuredClone(event));
320
+ });
321
+ ctx.on("session/flush", (session) => {
322
+ const failure = this.failures.get(session.id);
323
+ if (failure !== void 0) return Promise.reject(failure);
324
+ const handle = this.persistence.tracker.writerOf(session.id);
325
+ if (handle === void 0) {
326
+ const ready = this.ready.get(session.id);
327
+ if (ready === void 0) return void 0;
328
+ return ready.then(() => {
329
+ const settled = this.persistence.tracker.writerOf(session.id);
330
+ if (settled === void 0) return void 0;
331
+ return settled.drainLive().then(() => settled.flush());
332
+ });
333
+ }
334
+ return handle.drainLive().then(() => handle.flush());
335
+ });
336
+ ctx.on("session/disposed", (session) => {
337
+ const ready = this.ready.get(session.id);
338
+ this.forget(session.id);
339
+ const closeHandle = () => {
340
+ const handle = this.persistence.tracker.writerOf(session.id);
341
+ if (handle === void 0) return;
342
+ handle.close().catch((error) => {
343
+ ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
344
+ });
345
+ };
346
+ if (ready === void 0) {
347
+ closeHandle();
348
+ return;
349
+ }
350
+ ready.then(closeHandle, closeHandle);
351
+ });
352
+ ctx.effect(() => async () => {
353
+ await Promise.allSettled(this.ready.values());
354
+ this.buffers.clear();
355
+ this.ready.clear();
356
+ this.failures.clear();
357
+ this.dropWarned.clear();
358
+ await this.persistence.tracker.closeAll();
359
+ await this.persistence.close();
360
+ }, `${this.persistence.name} open handles`);
361
+ }
362
+ watchReady(session, ready) {
363
+ ready.catch((error) => {
364
+ const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
365
+ this.failures.set(session.id, failure);
366
+ this.buffers.delete(session.id);
367
+ this.ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
368
+ });
369
+ }
370
+ async ensureHandle(session) {
371
+ const persistence = this.persistence;
372
+ const id = session.header.id;
373
+ if (persistence.tracker.writerOf(id) !== void 0) return;
374
+ await persistence.ready;
375
+ const stored = await persistence.readLog(id, {});
376
+ let handle;
377
+ if (stored === void 0) {
378
+ handle = await persistence.create(session.header, { inheritedEventCount: session.inheritedEventCount });
379
+ const seed = liveEventsOf(session);
380
+ if (seed.length > 0) await handle.append(seed);
381
+ } else {
382
+ 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)`);
383
+ if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
384
+ assertVersion(stored.meta);
385
+ repairReadView(stored.events);
386
+ const seed = liveEventsOf(session);
387
+ 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)`);
388
+ handle = await persistence.open(id, "write");
389
+ const suffix = seed.slice(stored.events.length);
390
+ if (suffix.length > 0) await handle.append(suffix);
391
+ }
392
+ const buffered = this.buffers.get(id);
393
+ if (buffered !== void 0 && buffered.length > 0) {
394
+ this.buffers.set(id, []);
395
+ for (const event of buffered) handle.enqueueLive(event, () => {});
396
+ }
397
+ }
398
+ };
399
+ function seedCoversPrefix(seed, prefix) {
400
+ return prefix.length <= seed.length && prefix.every((event, index) => {
401
+ const seedEvent = seed[index];
402
+ return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
403
+ });
404
+ }
405
+ // src/read-path.ts
406
+ /**
407
+ * 读侧:会话日志、原始导出、快照列表与已存储 revision。
408
+ *
409
+ * 这些只读 `backend.reads`,不碰写事务;`session-store.ts` 的契约方法(`load` / `open` / `readFrom`)把调用转到这里。
410
+ */
411
+ function createReadPath(deps) {
412
+ return {
413
+ async readLog(id, options = {}, signal) {
414
+ signal?.throwIfAborted();
415
+ await deps.ready;
416
+ signal?.throwIfAborted();
417
+ const row = await deps.backend().reads.getSession(id);
418
+ if (row === void 0) return void 0;
419
+ const meta = rowToMeta(row);
420
+ const eventRows = options.fromSeq === void 0 ? await deps.backend().reads.getEventRows(id) : await deps.backend().reads.getEventRows(id, options.fromSeq);
421
+ signal?.throwIfAborted();
422
+ if (isLegacyVersion(row.fVersion)) try {
423
+ const converted = convertLegacyRows(row, eventRows);
424
+ return {
425
+ meta: converted.meta,
426
+ inheritedEventCount: converted.inheritedEventCount,
427
+ events: converted.events,
428
+ incarnation: row.fIncarnation,
429
+ revision: row.fRevision,
430
+ storedCount: eventRows.length,
431
+ migrated: true
432
+ };
433
+ } catch (error) {
434
+ deps.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)})`);
435
+ const adopted = adoptLegacyRows(row, eventRows);
436
+ return {
437
+ meta: adopted.meta,
438
+ inheritedEventCount: adopted.inheritedEventCount,
439
+ events: adopted.events,
440
+ incarnation: row.fIncarnation,
441
+ revision: row.fRevision,
442
+ storedCount: eventRows.length,
443
+ migrated: false,
444
+ ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
445
+ };
446
+ }
447
+ const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
448
+ if (needsShapeAdoption(preserved)) {
449
+ const adopted = adoptLegacyRows(row, eventRows);
450
+ return {
451
+ meta: adopted.meta,
452
+ inheritedEventCount: adopted.inheritedEventCount,
453
+ events: adopted.events,
454
+ incarnation: row.fIncarnation,
455
+ revision: row.fRevision,
456
+ storedCount: eventRows.length,
457
+ migrated: false,
458
+ ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
459
+ };
460
+ }
461
+ return {
462
+ meta,
463
+ inheritedEventCount: row.fSeedLength ?? 0,
464
+ events: preserved,
465
+ incarnation: row.fIncarnation,
466
+ revision: row.fRevision,
467
+ storedCount: eventRows.length,
468
+ migrated: false,
469
+ ...tornFrom !== void 0 ? { tornFrom } : {}
470
+ };
471
+ },
472
+ async readRaw(id, signal) {
473
+ signal?.throwIfAborted();
474
+ await deps.ready;
475
+ signal?.throwIfAborted();
476
+ const log = await this.readLog(id, {}, signal);
477
+ if (log === void 0) return void 0;
478
+ repairReadView(log.events);
479
+ const events = balanceRewindPrefix(log.events, { keepOpenTail: true });
480
+ const inheritedEventCount = Math.min(log.inheritedEventCount, events.length);
481
+ return {
482
+ meta: log.meta,
483
+ inheritedEventCount,
484
+ filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
485
+ content: toJsonlArtifact(log.meta, inheritedEventCount, events)
486
+ };
487
+ },
488
+ async listSnapshots(signal) {
489
+ signal?.throwIfAborted();
490
+ await deps.ready;
491
+ signal?.throwIfAborted();
492
+ const snapshots = [];
493
+ const listed = /* @__PURE__ */ new Set();
494
+ for (const [id, pending] of deps.tracker.pendingEntries()) {
495
+ snapshots.push({
496
+ header: pending.header,
497
+ revision: pending.revision,
498
+ inheritedEventCount: pending.inheritedEventCount
499
+ });
500
+ listed.add(id);
501
+ }
502
+ const rows = await deps.backend().reads.listSessions();
503
+ signal?.throwIfAborted();
504
+ for (const row of rows) {
505
+ if (listed.has(row.fSessionId)) continue;
506
+ snapshots.push({
507
+ header: rowToMeta(row),
508
+ revision: deps.revisionOf(row),
509
+ inheritedEventCount: row.fSeedLength ?? 0
510
+ });
511
+ }
512
+ return snapshots;
513
+ },
514
+ async readStoredRevision(id, signal) {
515
+ signal?.throwIfAborted();
516
+ await deps.ready;
517
+ signal?.throwIfAborted();
518
+ const row = await deps.backend().reads.getSession(id);
519
+ if (row === void 0) return void 0;
520
+ return deps.revisionOf(row);
521
+ }
522
+ };
523
+ }
524
+ // src/stats-fold.ts
525
+ function sessionCountBuckets(events) {
526
+ const counted = /* @__PURE__ */ new Map();
527
+ for (const event of events) {
528
+ if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
529
+ const day = localDayKey(event.time);
530
+ const bucket = counted.get(day) ?? {
531
+ day,
532
+ turns: 0,
533
+ steps: 0,
534
+ userInputs: 0,
535
+ toolCalls: 0
536
+ };
537
+ addActivityCount(bucket, event.type, 1);
538
+ counted.set(day, bucket);
539
+ }
540
+ return [...counted.values()];
541
+ }
542
+ function sessionUsageBuckets(usageRows) {
543
+ const counted = /* @__PURE__ */ new Map();
544
+ for (const row of usageRows) {
545
+ const provider = row.fProvider ?? "";
546
+ const model = row.fModel ?? "";
547
+ const key = `${row.fDay}#${provider}#${model}`;
548
+ const bucket = counted.get(key) ?? {
549
+ day: row.fDay,
550
+ provider,
551
+ model,
552
+ inputTokens: 0,
553
+ outputTokens: 0,
554
+ cacheReadTokens: 0,
555
+ reasoningTokens: 0,
556
+ totalTokens: 0
557
+ };
558
+ bucket.inputTokens += row.fInputTokens;
559
+ bucket.outputTokens += row.fOutputTokens;
560
+ bucket.cacheReadTokens += row.fCacheReadTokens;
561
+ bucket.reasoningTokens += row.fReasoningTokens;
562
+ bucket.totalTokens += row.fTotalTokens;
563
+ counted.set(key, bucket);
564
+ }
565
+ return [...counted.values()];
566
+ }
567
+ // src/write-path.ts
568
+ function maxEventTime(events) {
569
+ let max;
570
+ for (const event of events) {
571
+ const time = event.time;
572
+ if (typeof time !== "number") continue;
573
+ if (max === void 0 || time > max) max = time;
574
+ }
575
+ return max;
576
+ }
577
+ function describeError(error) {
578
+ return error instanceof Error ? error.message : "unknown error";
579
+ }
580
+ /**
581
+ * 写侧:空会话物化、批量追加、统计旁路累加与重算、迁移后重写。
582
+ *
583
+ * 事务边界都在这里;`session-store.ts` 只负责装配与契约转发。
584
+ */
585
+ function createWritePath(deps) {
586
+ return {
587
+ async materializeEmpty(meta, inheritedEventCount) {
588
+ await deps.ready;
589
+ await deps.backend().transaction(async (tx) => {
590
+ await tx.upsertSession({
591
+ meta,
592
+ inheritedEventCount
593
+ }, randomUUID());
594
+ await tx.bumpRevision(meta.id);
595
+ });
596
+ deps.tracker.materialized(meta.id);
597
+ deps.writeGuard.confirmHead(meta.id, -1);
598
+ },
599
+ async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
600
+ await deps.ready;
601
+ if (events.length === 0) return false;
602
+ sealOwnEvents(events);
603
+ validateStoredEvents(meta, [...events]);
604
+ const reuse = deps.reuseEventIds.get(meta.id);
605
+ let confirmedHead = -1;
606
+ let usageRows = [];
607
+ await deps.backend().transaction(async (tx) => {
608
+ if (tornTruncateTo !== void 0) {
609
+ await tx.deleteBridgeTail(meta.id, tornTruncateTo);
610
+ const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
611
+ if (prev === void 0) await tx.updateHead(meta.id, "", -1);
612
+ else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
613
+ }
614
+ await tx.upsertSession({
615
+ meta,
616
+ inheritedEventCount
617
+ }, randomUUID());
618
+ const head = await tx.getHead(meta.id);
619
+ deps.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
620
+ const appended = await appendEventTail(tx, meta, events, {
621
+ parentId: head.fHeadEventId,
622
+ nextSeq: head.fHeadSequence + 1
623
+ }, reuse);
624
+ usageRows = appended.usageRows;
625
+ await tx.updateHeadAndRevision(meta.id, appended.headEventId, appended.headSequence, maxEventTime(events));
626
+ confirmedHead = appended.headSequence;
627
+ });
628
+ if (reuse !== void 0) deps.reuseEventIds.delete(meta.id);
629
+ deps.writeGuard.confirmHead(meta.id, confirmedHead);
630
+ deps.tracker.materialized(meta.id);
631
+ await this.recordSessionStats(meta.id, events, usageRows);
632
+ return true;
633
+ },
634
+ async recordSessionStats(id, events, usageRows) {
635
+ const usageBuckets = sessionUsageBuckets(usageRows);
636
+ if (usageBuckets.length > 0) try {
637
+ await deps.backend().incrementSessionUsage(id, usageBuckets);
638
+ } catch (error) {
639
+ deps.logger.warn(`session-rdb: token usage for "${id}" not updated (${describeError(error)})`);
640
+ }
641
+ const countBuckets = sessionCountBuckets(events);
642
+ if (countBuckets.length > 0) try {
643
+ await deps.backend().incrementSessionCounts(id, countBuckets);
644
+ } catch (error) {
645
+ deps.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
646
+ }
647
+ },
648
+ async rebuildSessionStats(id) {
649
+ await deps.ready;
650
+ try {
651
+ await deps.backend().rebuildSessionStats(id);
652
+ } catch (error) {
653
+ deps.logger.warn(`session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`);
654
+ }
655
+ },
656
+ async refreshEventUsageFlags(eventIds) {
657
+ const unique = [...new Set(eventIds)];
658
+ if (unique.length === 0) return;
659
+ try {
660
+ await deps.backend().refreshEventUsageFlags(unique);
661
+ } catch (error) {
662
+ deps.logger.warn(`session-rdb: usage reference flags not refreshed (${describeError(error)})`);
663
+ }
664
+ },
665
+ async dropSessionStats(id) {
666
+ try {
667
+ await deps.backend().deleteSessionStats(id);
668
+ } catch (error) {
669
+ deps.logger.warn(`session-rdb: session stats for "${id}" not deleted (${describeError(error)})`);
670
+ }
671
+ },
672
+ async rewriteMigratedLog(id, log) {
673
+ let removed = [];
674
+ await deps.backend().transaction(async (tx) => {
675
+ const head = await tx.getHead(id);
676
+ if (!deps.writeGuard.has(id)) deps.writeGuard.confirmHead(id, head.fHeadSequence);
677
+ deps.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
678
+ removed = await tx.deleteBridgeTail(id, 0);
679
+ await tx.upsertSession({
680
+ meta: log.meta,
681
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
682
+ }, randomUUID());
683
+ const appended = await appendEventTail(tx, log.meta, log.events, {
684
+ parentId: "",
685
+ nextSeq: 0
686
+ });
687
+ await tx.updateHead(id, appended.headEventId, appended.headSequence);
688
+ await tx.bumpRevision(id, maxEventTime(log.events));
689
+ });
690
+ await this.rebuildSessionStats(id);
691
+ await this.refreshEventUsageFlags(removed);
692
+ }
693
+ };
694
+ }
695
+ async function appendEventTail(tx, meta, events, anchor, reuse) {
696
+ let parentId = anchor.parentId;
697
+ let nextSeq = anchor.nextSeq;
698
+ const eventRows = [];
699
+ const usageRows = [];
700
+ const bridgeRows = [];
701
+ for (const event of events) {
702
+ const reusedId = reuse?.get(event.seq);
703
+ const eventId = reusedId ?? randomUUID();
704
+ if (reusedId === void 0) {
705
+ const { kind, role, name, actionId } = eventDimensions(event);
706
+ const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
707
+ const row = {
708
+ fEventId: eventId,
709
+ fParentId: parentId,
710
+ fType: event.type,
711
+ fKind: kind,
712
+ fRole: role,
713
+ fName: name,
714
+ fActionId: actionId,
715
+ fEncoding: EVENT_ENCODING,
716
+ fData: JSON.stringify({
717
+ ...envelope,
718
+ data
719
+ }),
720
+ fCreatedAt: event.time
721
+ };
722
+ eventRows.push(row);
723
+ const usageRow = usageRowOf(row, meta.origin === "subagent");
724
+ if (usageRow !== void 0) usageRows.push(usageRow);
725
+ }
726
+ const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
727
+ bridgeRows.push({
728
+ fSessionId: meta.id,
729
+ fEventId: eventId,
730
+ fSequence: nextSeq,
731
+ fSurfaceOp: surfaceOp
732
+ });
733
+ parentId = eventId;
734
+ nextSeq++;
735
+ }
736
+ if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
737
+ await tx.insertBridges(bridgeRows);
738
+ if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
739
+ return {
740
+ headEventId: parentId,
741
+ headSequence: nextSeq - 1,
742
+ usageRows
743
+ };
744
+ }
745
+ // src/rows.ts
746
+ const MAX_PAGE_SIZE = 200;
747
+ const DEFAULT_PAGE_SIZE = 20;
748
+ function registerSessionRows(ctx, getBackend) {
749
+ mountJsonRoute(ctx, {
750
+ path: SESSION_ROWS_PATH,
751
+ handle: async (body) => {
752
+ const envelope = body;
753
+ const page = Number.isSafeInteger(envelope.page) && (envelope.page ?? 0) > 0 ? envelope.page : 1;
754
+ const requested = envelope.pageSize;
755
+ const pageSize = Number.isSafeInteger(requested) && (requested ?? 0) > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
756
+ const { items, total } = await (await getBackend()).listSessionRows({
757
+ ...envelope.query === void 0 ? {} : { query: envelope.query },
758
+ ...envelope.includeSubagents === void 0 ? {} : { includeSubagents: envelope.includeSubagents },
759
+ limit: pageSize,
760
+ offset: (page - 1) * pageSize
761
+ });
762
+ return { body: {
763
+ items,
764
+ total,
765
+ page,
766
+ pageSize
767
+ } };
768
+ },
769
+ statusOfError: () => void 0
770
+ });
771
+ }
772
+ // src/session-query.ts
773
+ var SessionQueryRdb = class extends SessionQueryEngine {
774
+ /**
775
+ * 本次装配用的冷会话观察缓存容量(由 `session-rdb` 行的 `preparedSessionCacheSize` 透传;undefined 表示沿用
776
+ * 上游默认)。它是**按条目数**持有完整 Session 的缓存:条目多一条就多一份完整 append-only log 驻留。
777
+ */
778
+ preparedSessionCacheSize;
779
+ constructor(ctx, config = {}) {
780
+ super(ctx, config);
781
+ this.preparedSessionCacheSize = config.preparedSessionCacheSize;
782
+ }
783
+ async listSessions(signal) {
784
+ const records = await super.listSessions(signal);
785
+ const archived = await this.archivedSessionIds();
786
+ if (archived.size === 0) return records;
787
+ return records.filter((record) => !archived.has(String(record.header.id)));
788
+ }
789
+ async archivedSessionIds() {
790
+ const registry = this.ctx.get("workspaceRegistry");
791
+ if (registry !== void 0) return new Set(registry.archivedSessionIds ?? []);
792
+ return await this.ctx.get("sessionPersistence")?.archivedSessionIds?.() ?? /* @__PURE__ */ new Set();
793
+ }
794
+ async searchSessions() {
795
+ throw searchDisabled();
796
+ }
797
+ async searchEvents() {
798
+ throw searchDisabled();
799
+ }
800
+ };
801
+ function searchDisabled() {
802
+ 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");
803
+ }
804
+ // src/storage-takeover/projection-cache.ts
805
+ function selects(keys, key) {
806
+ if (keys === void 0) return true;
807
+ if ("has" in keys) return keys.has(key);
808
+ return keys.includes(key);
809
+ }
810
+ const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
811
+ const PREDECESSOR_TITLE_KEY = "title";
812
+ var SessionProjectionCacheRdb = class extends Service {
813
+ static inject = ["sessionProjections", "sessions"];
814
+ records = /* @__PURE__ */ new Map();
815
+ queued = /* @__PURE__ */ new Set();
816
+ repository;
817
+ ready;
818
+ openRepository;
819
+ constructor(ctx, options) {
820
+ super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
821
+ this.openRepository = options.repository;
822
+ this.ready = options.ready;
823
+ }
824
+ async [Service.init]() {
825
+ this.installWritePath();
826
+ await this.ready;
827
+ this.repository = await this.openRepository();
828
+ const pruned = await this.repository.pruneStaleProjcache();
829
+ if (pruned > 0) this.ctx.logger.info(`session projection cache: pruned stale snapshot(s) for ${String(pruned)} session(s)`);
830
+ for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
831
+ }
832
+ lookup(id) {
833
+ return this.records.get(id);
834
+ }
835
+ recordFor(id, expected) {
836
+ const record = this.lookup(id);
837
+ if (record === void 0) return void 0;
838
+ return identityMatches(record.identity, expected) ? record : void 0;
839
+ }
840
+ cachedSnapshot(meta, keys) {
841
+ const record = this.recordForLifecycle(meta);
842
+ const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
843
+ return this.withDirectTitle(meta.id, keys, snapshot);
844
+ }
845
+ recordForLifecycle(meta) {
846
+ const record = this.lookup(meta.id);
847
+ return record === void 0 || !identityMatches(record.identity, lifecycleIdentityOf(meta)) ? void 0 : record;
848
+ }
849
+ withDirectTitle(id, keys, snapshot) {
850
+ if (!selects(keys, PREDECESSOR_TITLE_KEY)) return snapshot;
851
+ if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
852
+ const title = this.records.get(id)?.title;
853
+ if (title === void 0) return snapshot;
854
+ const values = {
855
+ ...snapshot?.values,
856
+ [PREDECESSOR_TITLE_KEY]: title.title
857
+ };
858
+ return {
859
+ asOfSeq: snapshot === void 0 ? title.seq : Math.min(snapshot.asOfSeq, title.seq),
860
+ values
861
+ };
862
+ }
863
+ cachedPredecessorTitle(meta) {
864
+ const record = this.lookup(meta.id);
865
+ if (record === void 0 || !predecessorIdentityMatches(record.identity, lifecycleIdentityOf(meta))) return;
866
+ return this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
867
+ }
868
+ viewRecord(record, keys) {
869
+ const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
870
+ const servedKeys = Object.keys(values);
871
+ if (servedKeys.length === 0) return void 0;
872
+ const firstKey = servedKeys[0];
873
+ let asOfSeq = record.rows[firstKey].seq;
874
+ for (const key of servedKeys.slice(1)) {
875
+ const row = record.rows[key];
876
+ if (row.seq < asOfSeq) asOfSeq = row.seq;
877
+ }
878
+ return {
879
+ asOfSeq,
880
+ values
881
+ };
882
+ }
883
+ hydratePrepared(session, events) {
884
+ const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
885
+ if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
886
+ try {
887
+ return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
888
+ } catch {
889
+ return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
890
+ }
891
+ }
892
+ async write(session) {
893
+ const rows = this.ctx.sessionProjections.checkpoint(session);
894
+ const previous = this.lookup(session.id)?.rows;
895
+ if (previous !== void 0 && sameCheckpoint(previous, rows)) return;
896
+ if (this.ctx.sessions.get(session.id) === session) try {
897
+ await this.ctx.sessions.flush(session);
898
+ } catch (error) {
899
+ this.ctx.logger.warn(`session projection cache: flush before checkpoint for "${session.id}" failed (checkpoint still written from the in-memory log): ${error instanceof Error ? error.message : String(error)}`);
900
+ }
901
+ const changed = changedRows(previous, rows);
902
+ const removedKeys = Object.keys(previous ?? {}).filter((key) => rows[key] === void 0);
903
+ await this.patch(session.id, identityOf(session.header, session.inheritedEventCount), changed, removedKeys);
904
+ }
905
+ async patch(id, identity, changed, removedKeys) {
906
+ const detachedChanged = detachJson(changed);
907
+ await this.repository.patchProjcache(id, detachedChanged, removedKeys);
908
+ const merged = {};
909
+ const previous = this.records.get(id)?.rows;
910
+ if (previous !== void 0) Object.assign(merged, previous);
911
+ Object.assign(merged, detachedChanged);
912
+ for (const key of removedKeys) delete merged[key];
913
+ const title = await this.repository.readSessionTitle(id);
914
+ this.records.set(id, {
915
+ sessionId: id,
916
+ identity,
917
+ rows: merged,
918
+ ...title === void 0 ? {} : { title }
919
+ });
920
+ }
921
+ async truncateTo(meta, inheritedEventCount, headSeq) {
922
+ const identity = identityOf(meta, inheritedEventCount);
923
+ const record = this.recordFor(meta.id, identity);
924
+ if (record === void 0) return;
925
+ const rows = Object.fromEntries(Object.entries(record.rows).filter(([, row]) => row.seq <= headSeq));
926
+ await this.put(meta.id, identity, rows);
927
+ }
928
+ coldSnapshot(meta, inheritedEventCount, events) {
929
+ const identity = identityOf(meta, inheritedEventCount);
930
+ const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
931
+ this.put(meta.id, identity, restored.checkpoint).catch((error) => {
932
+ this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
933
+ });
934
+ return restored.snapshot;
935
+ }
936
+ installWritePath() {
937
+ this.ctx.on("session/event", (session) => {
938
+ this.scheduleWrite(session, "event");
939
+ });
940
+ this.ctx.on("session/created", (session) => {
941
+ this.flushSoft(session, "create");
942
+ });
943
+ this.ctx.on("session/disposed", (session) => {
944
+ this.flushSoft(session, "detach");
945
+ this.queued.delete(session.id);
946
+ });
947
+ }
948
+ scheduleWrite(session, trigger) {
949
+ if (this.queued.has(session.id)) return;
950
+ this.queued.add(session.id);
951
+ queueMicrotask(() => {
952
+ this.queued.delete(session.id);
953
+ this.flushSoft(session, trigger);
954
+ });
955
+ }
956
+ async flushSoft(session, trigger) {
957
+ try {
958
+ await this.write(session);
959
+ } catch (error) {
960
+ this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
961
+ }
962
+ }
963
+ async put(id, identity, rows) {
964
+ const detached = detachJson(rows);
965
+ await this.repository.putProjcache(id, detached);
966
+ const title = await this.repository.readSessionTitle(id);
967
+ this.records.set(id, {
968
+ sessionId: id,
969
+ identity,
970
+ rows: detached,
971
+ ...title === void 0 ? {} : { title }
972
+ });
973
+ }
974
+ };
975
+ function sameRow(before, after) {
976
+ return before.ver === after.ver && JSON.stringify(before.val) === JSON.stringify(after.val);
977
+ }
978
+ function changedRows(previous, next) {
979
+ const changed = {};
980
+ for (const [key, row] of Object.entries(next)) {
981
+ const before = previous?.[key];
982
+ if (before === void 0 || !sameRow(before, row)) changed[key] = row;
983
+ }
984
+ return changed;
985
+ }
986
+ function sameCheckpoint(previous, next) {
987
+ const keys = Object.keys(next);
988
+ if (keys.length !== Object.keys(previous).length) return false;
989
+ for (const key of keys) {
990
+ const before = previous[key];
991
+ const after = next[key];
992
+ if (before === void 0 || after === void 0) return false;
993
+ if (!sameRow(before, after)) return false;
994
+ }
995
+ return true;
996
+ }
997
+ function detachJson(rows) {
998
+ let text;
999
+ try {
1000
+ text = JSON.stringify(rows);
1001
+ } catch (error) {
1002
+ throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
1003
+ }
1004
+ if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
1005
+ return JSON.parse(text);
1006
+ }
1007
+ function lifecycleIdentityOf(header) {
1008
+ return {
1009
+ formatVersion: header.version,
1010
+ createdAt: header.createdAt,
1011
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
1012
+ isSeeded: header.isSeeded
1013
+ };
1014
+ }
1015
+ function identityOf(header, inheritedEventCount) {
1016
+ const cut = SessionLogOffset(inheritedEventCount);
1017
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
1018
+ return {
1019
+ formatVersion: header.version,
1020
+ createdAt: header.createdAt,
1021
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
1022
+ isSeeded: header.isSeeded,
1023
+ inheritedEventCount: cut
1024
+ };
1025
+ }
1026
+ function identityMatches(stored, expected) {
1027
+ return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
1028
+ }
1029
+ function predecessorIdentityMatches(stored, expected) {
1030
+ return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
1031
+ }
1032
+ function lifecycleIdentityMatches(stored, expected) {
1033
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded;
1034
+ }
1035
+ // src/storage-takeover/storage-backend.ts
1036
+ var RdbStorageBackend = class {
1037
+ repository;
1038
+ kv = { open: (descriptor) => this.openUnit(descriptor) };
1039
+ open = /* @__PURE__ */ new Map();
1040
+ opening = /* @__PURE__ */ new Set();
1041
+ closed = false;
1042
+ constructor(repository) {
1043
+ this.repository = repository;
1044
+ }
1045
+ async openUnit(descriptor) {
1046
+ if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
1047
+ if (this.open.has(descriptor.name) || this.opening.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
1048
+ this.opening.add(descriptor.name);
1049
+ const repository = await this.repository();
1050
+ const unit = new RdbKvUnit(repository, descriptor);
1051
+ this.open.set(descriptor.name, unit);
1052
+ this.opening.delete(descriptor.name);
1053
+ try {
1054
+ const stored = await repository.readUnitVersion(descriptor.name);
1055
+ const accepted = stored === descriptor.version || (descriptor.compatibleVersions ?? []).includes(stored ?? -1);
1056
+ if (stored === void 0) await repository.insertUnitVersion(descriptor.name, descriptor.version);
1057
+ else if (!accepted) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${stored} on the medium, incompatible with descriptor version ${descriptor.version}`);
1058
+ } catch (error) {
1059
+ this.open.delete(descriptor.name);
1060
+ this.opening.delete(descriptor.name);
1061
+ throw error;
1062
+ }
1063
+ unit.onClose(() => {
1064
+ this.open.delete(descriptor.name);
1065
+ });
1066
+ return unit;
1067
+ }
1068
+ async close() {
1069
+ this.closed = true;
1070
+ const units = [...this.open.values()];
1071
+ this.open.clear();
1072
+ await Promise.all(units.map((unit) => unit.close()));
1073
+ }
1074
+ };
1075
+ var TrackedKvUnit = class {
1076
+ repository;
1077
+ descriptor;
1078
+ closed = false;
1079
+ inflight = /* @__PURE__ */ new Set();
1080
+ onClosed;
1081
+ constructor(repository, descriptor) {
1082
+ this.repository = repository;
1083
+ this.descriptor = descriptor;
1084
+ }
1085
+ onClose(release) {
1086
+ this.onClosed = release;
1087
+ }
1088
+ async close() {
1089
+ if (this.closed) return;
1090
+ this.closed = true;
1091
+ while (this.inflight.size > 0) await Promise.allSettled(this.inflight);
1092
+ this.onClosed?.();
1093
+ }
1094
+ async track(operation) {
1095
+ const settled = operation.then(() => void 0, () => void 0);
1096
+ this.inflight.add(settled);
1097
+ try {
1098
+ await operation;
1099
+ } finally {
1100
+ this.inflight.delete(settled);
1101
+ }
1102
+ }
1103
+ assertOpen() {
1104
+ if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
1105
+ }
1106
+ assertTable(table) {
1107
+ if (!this.descriptor.tables.includes(table)) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
1108
+ }
1109
+ };
1110
+ var RdbKvUnit = class extends TrackedKvUnit {
1111
+ async loadAll() {
1112
+ this.assertOpen();
1113
+ const tables = Object.create(null);
1114
+ for (const record of await this.repository.listDomainRecords(this.descriptor.name)) (tables[record.table] ??= {})[record.key] = record.value;
1115
+ for (const table of this.descriptor.tables) tables[table] ??= {};
1116
+ return {
1117
+ tables,
1118
+ global: this.descriptor.hasGlobal ? await this.repository.readDomainGlobal(this.descriptor.name) : null
1119
+ };
1120
+ }
1121
+ async putRecord(table, key, value) {
1122
+ this.assertOpen();
1123
+ this.assertTable(table);
1124
+ await this.track(this.repository.putDomainRecord(this.descriptor.name, table, key, value));
1125
+ }
1126
+ async deleteRecord(table, key) {
1127
+ this.assertOpen();
1128
+ this.assertTable(table);
1129
+ await this.track(this.repository.deleteDomainRecord(this.descriptor.name, table, key));
1130
+ }
1131
+ async deleteRecords(table, keys) {
1132
+ this.assertOpen();
1133
+ this.assertTable(table);
1134
+ await this.track(this.repository.deleteDomainRecords(this.descriptor.name, table, keys));
1135
+ }
1136
+ async setGlobal(value) {
1137
+ this.assertOpen();
1138
+ if (!this.descriptor.hasGlobal) throw new Error(`unit '${this.descriptor.name}' declares no global slot`);
1139
+ await this.track(this.repository.writeDomainGlobal(this.descriptor.name, value));
1140
+ }
1141
+ };
1142
+ // src/storage-takeover/index.ts
1143
+ function installStorageTakeover(ctx, options) {
1144
+ ctx.inject(["storage"], (storageCtx) => {
1145
+ const backend = new RdbStorageBackend(options.repository);
1146
+ storageCtx.effect(() => {
1147
+ const unregister = storageCtx.storage.backend.register("rdb", backend);
1148
+ return async () => {
1149
+ unregister();
1150
+ await backend.close();
1151
+ };
1152
+ }, "session-rdb.storageBackend");
1153
+ storageCtx.provide(storageBackendServiceKey("rdb"), backend);
1154
+ });
1155
+ ctx.plugin(SessionProjectionCacheRdb, {
1156
+ repository: options.repository,
1157
+ ready: options.ready
1158
+ });
1159
+ }
1160
+ // src/session-store.ts
1161
+ const localized = (text) => text;
1162
+ const preparedSessionCacheSchema = z.number().step(1).min(1).default(1).description(localized({
1163
+ zh: "冷会话观察缓存容量(条数,最小 1):每个条目持一份完整会话日志;本部署默认 1,把同 id 两份日志的驻留压到最小。",
1164
+ en: "Cold prepared-Session observation cache size (entries, minimum 1): each entry holds one complete session log. This deployment defaults to 1, minimizing duplicate per-id residency."
1165
+ }));
1166
+ var SessionPersistenceRdb = class extends SessionPersistence {
1167
+ static inject = ["sessions"];
1168
+ static Config = z.union([z.object({
1169
+ type: z.const("sqlite"),
1170
+ path: z.string().required().description(localized({
1171
+ zh: "SQLite 数据库文件路径;`:memory:` 是内存库(进程结束即丢)。",
1172
+ en: "SQLite database file path; `:memory:` keeps it in memory and loses it with the process."
1173
+ })),
1174
+ journalMode: z.union([
1175
+ "wal",
1176
+ "delete",
1177
+ "truncate",
1178
+ "persist"
1179
+ ]).default("wal").description(localized({
1180
+ zh: "SQLite 日志模式;默认 wal(并发读最好)。",
1181
+ en: "SQLite journal mode; `wal` by default (best concurrent reads)."
1182
+ })),
1183
+ busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS).description(localized({
1184
+ zh: "写锁冲突时的忙等超时(毫秒)。",
1185
+ en: "Busy timeout in ms while a write lock is contended."
1186
+ })),
1187
+ preparedSessionCacheSize: preparedSessionCacheSchema
1188
+ }), z.object({
1189
+ type: z.const("postgres"),
1190
+ connectionString: z.string().required().description(localized({
1191
+ zh: "PostgreSQL 连接串(含库名与凭证)。",
1192
+ en: "PostgreSQL connection string (database and credentials)."
1193
+ })),
1194
+ schema: z.string().default("public").description(localized({
1195
+ zh: "库内的 schema 名;默认 public。",
1196
+ en: "Schema name inside the database; `public` by default."
1197
+ })),
1198
+ preparedSessionCacheSize: preparedSessionCacheSchema
1199
+ })]).description(localized({
1200
+ zh: "会话持久化后端:改这里的连接参数会在这一行重挂时生效(Loader 会重装被改的行)。",
1201
+ en: "Session persistence backend: edits apply when this row is remounted (the Loader re-installs changed rows)."
1202
+ })).volatile();
1203
+ name = "session-rdb";
1204
+ tracker = new RdbBackendTracker(this.name);
1205
+ /** 打开后的后端;未打开时访问会 fail loud(装配内部、分支端口与测试共用)。 */
1206
+ backend;
1207
+ storeIdentity;
1208
+ /** 后端打开完成(分支端口与 live 路由都先等它)。 */
1209
+ ready;
1210
+ /** 写者位点守卫(分支的 rewind 与写路径共用)。 */
1211
+ writeGuard = new WriteGuard();
1212
+ live;
1213
+ readPath;
1214
+ writePath;
1215
+ reuseEventIds = /* @__PURE__ */ new Map();
1216
+ config;
1217
+ constructor(ctx, config) {
1218
+ super(ctx);
1219
+ this.config = config.get();
1220
+ this.ready = this.init();
1221
+ this.readPath = createReadPath({
1222
+ ready: this.ready,
1223
+ backend: () => this.backend,
1224
+ tracker: this.tracker,
1225
+ logger: ctx.logger,
1226
+ revisionOf: (row) => this.rowRevision(row)
1227
+ });
1228
+ this.writePath = createWritePath({
1229
+ ready: this.ready,
1230
+ backend: () => this.backend,
1231
+ tracker: this.tracker,
1232
+ writeGuard: this.writeGuard,
1233
+ reuseEventIds: this.reuseEventIds,
1234
+ logger: ctx.logger,
1235
+ readLog: async (id) => {
1236
+ const log = await this.readPath.readLog(id, {});
1237
+ return log === void 0 ? void 0 : {
1238
+ meta: log.meta,
1239
+ inheritedEventCount: log.inheritedEventCount,
1240
+ events: log.events
1241
+ };
1242
+ }
1243
+ });
1244
+ this.live = new LiveRouting(ctx, this);
1245
+ this.live.install();
1246
+ new SessionBranchRdb(this.ctx);
1247
+ const queryConfig = this.config.preparedSessionCacheSize === void 0 ? {} : { preparedSessionCacheSize: this.config.preparedSessionCacheSize };
1248
+ this.ctx.plugin(SessionQueryRdb, queryConfig);
1249
+ registerSessionImport(this.ctx, this);
1250
+ registerSessionDeletion(this.ctx, this);
1251
+ registerSessionExport(this.ctx, this);
1252
+ registerSessionOrganize(this.ctx, this);
1253
+ registerSessionUsage(this.ctx, this);
1254
+ registerSessionRows(this.ctx, () => this.database());
1255
+ installStorageTakeover(this.ctx, {
1256
+ repository: () => this.database().then((backend) => backend.storage),
1257
+ ready: this.ready
1258
+ });
1259
+ }
1260
+ /** 打开完成后的 backend(所有 DB 操作的门)。 */
1261
+ async database() {
1262
+ await this.ready;
1263
+ return this.backend;
1264
+ }
1265
+ async init() {
1266
+ this.backend = await openRdbBackend(backendConfigOf(this.config));
1267
+ await this.backend.open();
1268
+ this.storeIdentity = this.backend.storeIdentity;
1269
+ }
1270
+ async create(header, options) {
1271
+ options?.signal?.throwIfAborted();
1272
+ const snapshot = materializeCreateHeader(header);
1273
+ if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
1274
+ const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
1275
+ if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
1276
+ await this.ready;
1277
+ options?.signal?.throwIfAborted();
1278
+ if (this.tracker.hasPending(snapshot.id) || await this.backend.reads.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
1279
+ this.tracker.registerCreated(snapshot, inheritedEventCount);
1280
+ return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
1281
+ cursor: 0,
1282
+ materialized: false,
1283
+ inheritedEventCount
1284
+ }));
1285
+ }
1286
+ async open(id, access, options) {
1287
+ options?.signal?.throwIfAborted();
1288
+ await this.ready;
1289
+ options?.signal?.throwIfAborted();
1290
+ const pending = this.tracker.pendingOf(id);
1291
+ if (access === "read") {
1292
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
1293
+ cursor: 0,
1294
+ materialized: false,
1295
+ inheritedEventCount: pending.inheritedEventCount
1296
+ }));
1297
+ const log = await this.readPath.readLog(id, {}, options?.signal);
1298
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1299
+ repairReadView(log.events);
1300
+ validateStoredEvents(log.meta, log.events);
1301
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
1302
+ cursor: log.events.length,
1303
+ materialized: true,
1304
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1305
+ }));
1306
+ }
1307
+ this.tracker.claimWrite(id);
1308
+ try {
1309
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
1310
+ cursor: pending.cursor,
1311
+ materialized: false,
1312
+ inheritedEventCount: pending.inheritedEventCount
1313
+ }));
1314
+ const log = await this.readPath.readLog(id, {}, options?.signal);
1315
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1316
+ repairReadView(log.events);
1317
+ validateStoredEvents(log.meta, log.events);
1318
+ if (log.migrated && log.events.length !== log.storedCount) await this.writePath.rewriteMigratedLog(id, log);
1319
+ this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
1320
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
1321
+ cursor: log.events.length,
1322
+ materialized: true,
1323
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1324
+ ...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
1325
+ }));
1326
+ } catch (error) {
1327
+ this.tracker.releaseClaim(id);
1328
+ throw error;
1329
+ }
1330
+ }
1331
+ flush() {
1332
+ return this.tracker.flushAll();
1333
+ }
1334
+ async stat(id, options) {
1335
+ options?.signal?.throwIfAborted();
1336
+ await this.ready;
1337
+ options?.signal?.throwIfAborted();
1338
+ const pending = this.tracker.pendingOf(id);
1339
+ if (pending !== void 0) return {
1340
+ header: pending.header,
1341
+ revision: pending.revision
1342
+ };
1343
+ const row = await this.backend.reads.getSession(id);
1344
+ if (row === void 0) return void 0;
1345
+ return {
1346
+ header: rowToMeta(row),
1347
+ revision: this.rowRevision(row)
1348
+ };
1349
+ }
1350
+ async list(options) {
1351
+ const signal = options?.signal;
1352
+ const snapshots = [];
1353
+ const listed = /* @__PURE__ */ new Set();
1354
+ for (const [id, pending] of this.tracker.pendingEntries()) {
1355
+ snapshots.push({
1356
+ header: pending.header,
1357
+ revision: pending.revision
1358
+ });
1359
+ listed.add(id);
1360
+ }
1361
+ signal?.throwIfAborted();
1362
+ await this.ready;
1363
+ signal?.throwIfAborted();
1364
+ const rows = await this.backend.reads.listSessions();
1365
+ signal?.throwIfAborted();
1366
+ for (const row of rows) {
1367
+ if (listed.has(row.fSessionId)) continue;
1368
+ snapshots.push({
1369
+ header: rowToMeta(row),
1370
+ revision: this.rowRevision(row)
1371
+ });
1372
+ }
1373
+ return snapshots;
1374
+ }
1375
+ async deleteSession(id, signal) {
1376
+ signal?.throwIfAborted();
1377
+ await this.ready;
1378
+ signal?.throwIfAborted();
1379
+ const row = await this.backend.reads.getSession(id);
1380
+ if (row === void 0) throw new SessionDeletionError(`session "${id}" not found`, "SESSION_NOT_FOUND");
1381
+ if (row.fArchivedAt === null) throw new SessionDeletionError(`session "${id}" is not archived; only archived sessions can be deleted`, "SESSION_NOT_ARCHIVED");
1382
+ if (this.tracker.hasPending(id) || this.tracker.hasOpenHandle(id)) throw new SessionDeletionError(`session "${id}" is live; stop it before deleting`, "SESSION_LIVE");
1383
+ await this.unarchiveBeforeDeletion(id);
1384
+ const referenced = await this.backend.transaction((tx) => tx.deleteSession(id));
1385
+ this.live.forget(id);
1386
+ this.reuseEventIds.delete(id);
1387
+ await this.writePath.dropSessionStats(id);
1388
+ await this.writePath.refreshEventUsageFlags(referenced);
1389
+ }
1390
+ /**
1391
+ * 整理数据:回收孤儿会话与孤儿事件行,然后**整表重算**三张派生统计表,最后 VACUUM。
1392
+ *
1393
+ * 顺序有依赖:先清孤儿(被删会话独占的事件行才刚成为孤儿),再按事件表重算统计,
1394
+ * 否则统计会把刚删掉的引用算回去。
1395
+ * @returns 三项计数(孤儿会话 / 孤儿事件行 / 重建涉及的会话数)。
1396
+ */
1397
+ async organize() {
1398
+ await this.ready;
1399
+ const orphanSessions = await this.collectOrphanSessions();
1400
+ const orphanEvents = await this.collectOrphans();
1401
+ const rebuiltSessions = await this.backend.rebuildAllStats();
1402
+ await this.backend.vacuum();
1403
+ return {
1404
+ orphanSessions,
1405
+ orphanEvents,
1406
+ rebuiltSessions
1407
+ };
1408
+ }
1409
+ async collectOrphans() {
1410
+ await this.ready;
1411
+ return this.backend.collectOrphans();
1412
+ }
1413
+ async collectOrphanSessions() {
1414
+ await this.ready;
1415
+ const deletable = (await this.backend.listOrphanSubagentSessions()).filter((id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id));
1416
+ if (deletable.length === 0) return 0;
1417
+ const { deleted, eventIds } = await this.backend.transaction((tx) => tx.deleteSessions(deletable));
1418
+ for (const id of deletable) {
1419
+ this.live.forget(id);
1420
+ this.reuseEventIds.delete(id);
1421
+ await this.writePath.dropSessionStats(id);
1422
+ }
1423
+ await this.writePath.refreshEventUsageFlags(eventIds);
1424
+ return deleted;
1425
+ }
1426
+ async usageReport(sinceMs) {
1427
+ await this.ready;
1428
+ return this.backend.usageReport(sinceMs);
1429
+ }
1430
+ async vacuum() {
1431
+ await this.ready;
1432
+ await this.backend.vacuum();
1433
+ }
1434
+ /**
1435
+ * 已归档的会话 id(`t_sessions.f_archived_at` 非空)。`session/list` 默认过滤掉它们。
1436
+ * 读的是持久化里的归档标记,不依赖 `workspaceRegistry` 服务的就绪时机。
1437
+ * @returns 归档过的 Session id 集。
1438
+ */
1439
+ async archivedSessionIds() {
1440
+ await this.ready;
1441
+ const state = await this.backend.storage.readDomainGlobal("workspace");
1442
+ return new Set((state?.archivedSessionIds ?? []).map(String));
1443
+ }
1444
+ async unarchiveBeforeDeletion(id) {
1445
+ const registry = this.ctx.get("workspaceRegistry");
1446
+ if (registry === void 0) return;
1447
+ if (!registry.archivedSessionIds.includes(id)) return;
1448
+ await registry.unarchiveSession(id);
1449
+ }
1450
+ readLog(id, options = {}, signal) {
1451
+ return this.readPath.readLog(id, options, signal);
1452
+ }
1453
+ readRaw(id, signal) {
1454
+ return this.readPath.readRaw(id, signal);
1455
+ }
1456
+ listSnapshots(signal) {
1457
+ return this.readPath.listSnapshots(signal);
1458
+ }
1459
+ readStoredRevision(id, signal) {
1460
+ return this.readPath.readStoredRevision(id, signal);
1461
+ }
1462
+ materializeEmpty(meta, inheritedEventCount) {
1463
+ return this.writePath.materializeEmpty(meta, inheritedEventCount);
1464
+ }
1465
+ appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
1466
+ return this.writePath.appendBatch(meta, inheritedEventCount, events, tornTruncateTo);
1467
+ }
1468
+ async close() {
1469
+ await this.ready;
1470
+ await this.backend.close();
1471
+ }
1472
+ async createAndAppend(header, events, inheritedEventCount) {
1473
+ const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
1474
+ try {
1475
+ if (events.length > 0) await handle.append(events);
1476
+ } finally {
1477
+ await handle.close();
1478
+ }
1479
+ }
1480
+ async load(id, signal) {
1481
+ signal?.throwIfAborted();
1482
+ await this.ready;
1483
+ signal?.throwIfAborted();
1484
+ const log = await this.readPath.readLog(id, {}, signal);
1485
+ if (log === void 0) {
1486
+ const pending = this.tracker.pendingOf(id);
1487
+ if (pending !== void 0) return {
1488
+ meta: pending.header,
1489
+ inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
1490
+ events: []
1491
+ };
1492
+ throw new SessionPersistenceNotFoundError(id);
1493
+ }
1494
+ repairReadView(log.events);
1495
+ validateStoredEvents(log.meta, log.events);
1496
+ return {
1497
+ meta: log.meta,
1498
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1499
+ events: log.events
1500
+ };
1501
+ }
1502
+ async append(id, events) {
1503
+ const handle = await this.open(id, "write");
1504
+ try {
1505
+ await handle.append(events);
1506
+ } finally {
1507
+ await handle.close();
1508
+ }
1509
+ }
1510
+ async readFrom(id, fromSeq, signal) {
1511
+ const log = await this.readPath.readLog(id, { fromSeq }, signal);
1512
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1513
+ return {
1514
+ meta: log.meta,
1515
+ inheritedEventCount: log.inheritedEventCount,
1516
+ events: log.events
1517
+ };
1518
+ }
1519
+ registerReuseEventIds(childId, map) {
1520
+ this.reuseEventIds.set(childId, new Map(map));
1521
+ }
1522
+ dropReuseEventIds(childId) {
1523
+ this.reuseEventIds.delete(childId);
1524
+ }
1525
+ /** 分支能力的端口(见 `branch/port.ts`):每个动词都有真实调用方,不是服务方法表的转抄。 */
1526
+ branchPort() {
1527
+ return {
1528
+ ready: this.ready,
1529
+ readLog: (id, options, signal) => this.readPath.readLog(id, options ?? {}, signal),
1530
+ create: (meta, options) => this.create(meta, options),
1531
+ eventRows: (id, fromSequence) => this.backend.reads.getEventRows(id, fromSequence),
1532
+ eventTypesBefore: (id, before, limit) => this.backend.reads.getEventTypesBefore(id, before, limit),
1533
+ sessionRow: (id) => this.backend.reads.getSession(id),
1534
+ eventTypeAt: (id, sequence) => this.backend.reads.getEventTypeAt(id, sequence),
1535
+ transaction: (fn) => this.backend.transaction(fn),
1536
+ confirmHead: (id, sequence) => this.writeGuard.confirmHead(id, sequence),
1537
+ readStoredRevision: (id) => this.readPath.readStoredRevision(id),
1538
+ registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
1539
+ dropReuseEventIds: (childId) => this.dropReuseEventIds(childId),
1540
+ resetWriterAfterRewind: (id, cursor, inheritedEventCount) => {
1541
+ this.tracker.writerOf(id)?.resetAfterRewind(cursor, inheritedEventCount);
1542
+ }
1543
+ };
1544
+ }
1545
+ rowRevision(row) {
1546
+ return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
1547
+ }
1548
+ };
1549
+ /** 装配层配置 → 打开后端所需的最小形状(两方言共用)。 */
1550
+ function backendConfigOf(config) {
1551
+ return config.type === "sqlite" ? {
1552
+ type: "sqlite",
1553
+ path: config.path,
1554
+ journalMode: config.journalMode ?? "wal",
1555
+ busyTimeout: config.busyTimeout ?? 5e3
1556
+ } : {
1557
+ type: "postgres",
1558
+ connectionString: config.connectionString,
1559
+ ...config.schema === void 0 ? {} : { schema: config.schema }
1560
+ };
1561
+ }
5
1562
  export { SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionPersistenceRdb, SessionPersistenceRdb as default, liveEventsOf, locateTurnEnd };