@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/src/index.ts CHANGED
@@ -1,1666 +1,15 @@
1
- import { Context } from "@deepseek-ai/cordis";
2
- import type { Volatile } from "@deepseek-ai/cosmokit";
3
- import z from "@deepseek-ai/schemastery";
4
- import { randomUUID } from "node:crypto";
5
- import { Pool } from "pg";
6
- import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
7
- import {
8
- SessionAlreadyExistsError,
9
- SessionAlreadyOwnedError,
10
- SessionHandleClosedError,
11
- SessionPersistence,
12
- SessionPersistenceNotFoundError,
13
- SessionPersistenceRevision,
14
- SessionReadOnlyError,
15
- assertContiguous,
16
- assertVersion,
17
- materializeAppendBatch,
18
- materializeCreateHeader,
19
- validateStoredEvents,
20
- type SessionAccess,
21
- type SessionHandle,
22
- type SessionHandleAppendOptions,
23
- type SessionHandleFlushOptions,
24
- type SessionHandleReadOptions,
25
- type SessionHandleReadResult,
26
- type SessionPersistenceCreateOptions,
27
- type SessionPersistenceListOptions,
28
- type SessionPersistenceOpenOptions,
29
- type SessionPersistenceSnapshot,
30
- type SessionPersistenceStatOptions,
31
- } from "@deepseek-ai/dsh-session-persistence";
32
- import {
33
- SESSION_FORMAT_VERSION,
34
- SessionLogOffset,
35
- type Session,
36
- type SessionEvent,
37
- type SessionHeader,
38
- type SessionId,
39
- type SurfaceEventType,
40
- } from "@deepseek-ai/dsh-session";
41
- import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
42
- import {
43
- type Backend,
44
- type BackendTx,
45
- type EventInsert,
46
- type SessionCountBucket,
47
- type SessionUsageBucket,
48
- } from "./backend.ts";
49
- import { WriteGuard } from "./write-guard.ts";
50
- import { repairReadView, rowToMeta, scanRows, toJsonlArtifact, usageRowOf } from "./log.ts";
51
- import type { EventUsageRow } from "./log.ts";
52
- import {
53
- DEFAULT_BUSY_TIMEOUT_MS,
54
- eventDimensions,
55
- EVENT_ENCODING,
56
- type JournalMode,
57
- } from "./schema.ts";
58
- import { SqliteBackend } from "./sqlite.ts";
59
- import { PostgresBackend } from "./postgres.ts";
60
- import { SessionBranchRdb, liveEventsOf } from "./branch.ts";
61
- import { balanceRewindPrefix } from "@morlay/session-branch";
62
- import { registerSessionImport } from "./import.ts";
63
- import { registerSessionDeletion } from "./deletion.ts";
64
- import { registerSessionExport } from "./export.ts";
65
- import { registerSessionGc } from "./gc.ts";
66
- import { registerSessionRows } from "./rows.ts";
67
- import {
68
- COUNTED_EVENT_TYPES,
69
- addActivityCount,
70
- localDayKey,
71
- registerSessionUsage,
72
- } from "./usage.ts";
73
- import type { UsageAggregate } from "./usage.ts";
74
- import { SessionQueryRdb } from "./session-query.ts";
75
- import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
76
- import { needsShapeAdoption, sealOwnEvents } from "./log.ts";
77
- import { installStorageTakeover } from "./storage-takeover/index.ts";
78
-
79
- // 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。
80
- function maxEventTime(events: readonly { readonly time?: number }[]): number | undefined {
81
- let max: number | undefined;
82
- for (const event of events) {
83
- const time = event.time;
84
- if (typeof time !== "number") continue;
85
- if (max === undefined || time > max) max = time;
86
- }
87
- return max;
88
- }
89
-
90
- const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
91
- const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5000;
92
-
93
- export { SCHEMA_VERSION } from "./schema.ts";
94
- export { SESSION_ROWS_PATH } from "./rows.ts";
95
- export type { SessionRowsItem, SessionRowsValue } from "./rows.ts";
96
- export { SessionBranchRdb, SessionBranchRdbProvider, liveEventsOf, locateTurnEnd } from "./branch.ts";
97
-
98
- export interface SessionPersistenceRdbInternals {
99
- readonly backend: Backend;
100
- readonly writeGuard: WriteGuard;
101
- create(meta: SessionHeader, inheritedEventCount?: number): Promise<SessionHandle>;
102
- append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
103
- load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
104
- inspect(
105
- id: SessionId,
106
- signal?: AbortSignal,
107
- ): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
108
- readFrom(
109
- id: SessionId,
110
- fromSeq: number,
111
- signal?: AbortSignal,
112
- ): Promise<{ meta: SessionHeader; inheritedEventCount: number; events: readonly SessionEvent[] }>;
113
- listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
114
- readStoredRevision(
115
- id: SessionId,
116
- signal?: AbortSignal,
117
- ): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
118
-
119
- registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
120
- // fork 失败时丢弃还没被消费的映射。
121
- dropReuseEventIds(childId: SessionId): void;
122
- }
123
-
124
- export interface ProjectionCacheOptions {
125
- writeEveryEvents?: number;
126
-
127
- writeIntervalMs?: number;
128
- }
129
-
130
- export type Config =
131
- | {
132
- type: "sqlite";
133
- path: string;
134
- journalMode?: JournalMode | undefined;
135
- busyTimeout?: number | undefined;
136
- projectionCache?: ProjectionCacheOptions | undefined;
137
- preparedSessionCacheSize?: number | undefined;
138
- }
139
- | {
140
- type: "postgres";
141
- connectionString: string;
142
- schema?: string | undefined;
143
- projectionCache?: ProjectionCacheOptions | undefined;
144
- preparedSessionCacheSize?: number | undefined;
145
- };
146
-
147
- // schema 解析之后的形状:整段 Config 是 **volatile 稳定引用**(页面可编辑),读它要过 `.get()`。
148
- export type ResolvedConfig = Volatile<Config>;
149
-
150
- export type SessionDeletionErrorCode =
151
- | "SESSION_NOT_FOUND"
152
- | "SESSION_NOT_ARCHIVED"
153
- | "SESSION_LIVE";
154
-
155
- export class SessionDeletionError extends Error {
156
- constructor(
157
- message: string,
158
- readonly code: SessionDeletionErrorCode,
159
- ) {
160
- super(message);
161
- this.name = "SessionDeletionError";
162
- }
163
- }
164
-
165
- interface PendingSession {
166
- readonly header: SessionHeader;
167
- readonly revision: SessionPersistenceRevision;
168
- readonly inheritedEventCount: SessionLogOffset;
169
-
170
- readonly cursor: number;
171
-
172
- readonly everAppended: boolean;
173
- }
174
-
175
- class RdbBackendTracker {
176
- private readonly writers = new Map<SessionId, RdbSessionHandle | null>();
177
- private readonly pending = new Map<SessionId, PendingSession>();
178
- private readonly openHandles = new Set<RdbSessionHandle>();
179
- private counter = 0;
180
-
181
- constructor(private readonly name: string) {}
182
-
183
- registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void {
184
- if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
185
- this.writers.set(header.id, null);
186
- this.pending.set(header.id, {
187
- header,
188
- revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
189
- inheritedEventCount,
190
- cursor: 0,
191
- everAppended: false,
192
- });
193
- }
194
-
195
- updatePending(id: SessionId, cursor: number, everAppended: boolean): void {
196
- const entry = this.pending.get(id);
197
- if (entry === undefined) return;
198
- this.pending.set(id, { ...entry, cursor, everAppended });
199
- }
200
-
201
- claimWrite(id: SessionId): void {
202
- if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
203
- this.writers.set(id, null);
204
- }
205
-
206
- releaseClaim(id: SessionId): void {
207
- this.writers.delete(id);
208
- }
209
-
210
- pendingOf(id: SessionId): PendingSession | undefined {
211
- return this.pending.get(id);
212
- }
213
-
214
- hasPending(id: SessionId): boolean {
215
- return this.pending.has(id);
216
- }
217
-
218
- pendingEntries(): IterableIterator<[SessionId, PendingSession]> {
219
- return this.pending.entries();
220
- }
221
-
222
- materialized(id: SessionId): void {
223
- this.pending.delete(id);
224
- }
225
-
226
- adopt(handle: RdbSessionHandle): RdbSessionHandle {
227
- this.openHandles.add(handle);
228
- if (handle.access === "write") this.writers.set(handle.id, handle);
229
- return handle;
230
- }
231
-
232
- release(handle: RdbSessionHandle, materialized: boolean): void {
233
- this.openHandles.delete(handle);
234
- if (handle.access !== "write") return;
235
- this.writers.delete(handle.id);
236
- if (!materialized) this.pending.delete(handle.id);
237
- }
238
-
239
- writerOf(id: SessionId): RdbSessionHandle | undefined {
240
- const writer = this.writers.get(id);
241
- return writer === null ? undefined : writer;
242
- }
243
-
244
- hasOpenHandle(id: SessionId): boolean {
245
- for (const handle of this.openHandles) {
246
- if (handle.id === id) return true;
247
- }
248
- return false;
249
- }
250
-
251
- async flushAll(): Promise<void> {
252
- const errors: unknown[] = [];
253
- for (const writer of this.writers.values()) {
254
- if (writer === null) continue;
255
- try {
256
- await writer.drainLive();
257
- await writer.flush();
258
- } catch (error: unknown) {
259
- if (error instanceof SessionHandleClosedError) continue;
260
- errors.push(error);
261
- }
262
- }
263
- if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
264
- }
265
-
266
- async closeAll(): Promise<void> {
267
- const errors: unknown[] = [];
268
- for (const handle of this.openHandles) {
269
- try {
270
- await handle.close();
271
- } catch (error: unknown) {
272
- errors.push(error);
273
- }
274
- }
275
- if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
276
- }
277
- }
278
-
279
- class RdbSessionHandle implements SessionHandle {
280
- private chain: Promise<unknown> = Promise.resolve();
281
- private closing: Promise<void> | undefined;
282
-
283
- private cursor: number;
284
- private materialized: boolean;
285
-
286
- private buffered: SessionEvent[] = [];
287
- private batchTimer: ReturnType<typeof setTimeout> | undefined;
288
- private drainPaused = false;
289
- private draining: Promise<void> | undefined;
290
-
291
- private tornTruncateTo: number | undefined;
292
-
293
- private everAppended = false;
294
-
295
- constructor(
296
- private readonly persistence: SessionPersistenceRdb,
297
- readonly id: SessionId,
298
- readonly header: SessionHeader,
299
- readonly access: SessionAccess,
300
- private readonly state: {
301
- cursor: number;
302
- materialized: boolean;
303
- inheritedEventCount: SessionLogOffset;
304
- tornTruncateTo?: number;
305
- },
306
- ) {
307
- this.cursor = state.cursor;
308
- this.materialized = state.materialized;
309
- this.tornTruncateTo = state.tornTruncateTo;
310
- }
311
-
312
- get inheritedEventCount(): SessionLogOffset {
313
- return this.state.inheritedEventCount;
314
- }
315
-
316
- get cursorValue(): number {
317
- return this.cursor;
318
- }
319
-
320
- async read(
321
- offset = 0,
322
- length = Number.MAX_SAFE_INTEGER,
323
- options?: SessionHandleReadOptions,
324
- ): Promise<SessionHandleReadResult> {
325
- this.assertOpen("read");
326
- if (!Number.isSafeInteger(offset) || offset < 0) {
327
- throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
328
- }
329
- if (!Number.isSafeInteger(length) || length < 0) {
330
- throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
331
- }
332
- options?.signal?.throwIfAborted();
333
- const log = await this.persistence.readLog(this.id, {}, options?.signal);
334
- if (log === undefined) {
335
- if (this.persistence.tracker.hasPending(this.id)) {
336
- return { eventState: "detached", events: [] };
337
- }
338
- throw new SessionPersistenceNotFoundError(this.id);
339
- }
340
-
341
- repairReadView(log.events);
342
- return { eventState: "detached", events: log.events.slice(offset, offset + length) };
343
- }
344
-
345
- async append(
346
- events: readonly SessionEvent[],
347
- options?: SessionHandleAppendOptions,
348
- ): Promise<void> {
349
- this.assertOpen("append");
350
- const batch = materializeAppendBatch(events);
351
- return this.run("append", async () => {
352
- options?.signal?.throwIfAborted();
353
- if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
354
- if (batch.length === 0) return;
355
- this.everAppended = true;
356
- assertContiguous(this.id, batch, this.cursor);
357
- await this.persistence.appendBatch(
358
- this.header,
359
- this.state.inheritedEventCount,
360
- batch,
361
- this.tornTruncateTo,
362
- );
363
- this.tornTruncateTo = undefined;
364
-
365
- this.cursor += batch.length;
366
- this.materialized = true;
367
- this.persistence.tracker.updatePending(this.id, this.cursor, true);
368
- });
369
- }
370
-
371
- async flush(options?: SessionHandleFlushOptions): Promise<void> {
372
- return this.run("flush", async () => {
373
- options?.signal?.throwIfAborted();
374
- if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
375
- if (this.materialized) return;
376
- await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
377
- this.materialized = true;
378
- });
379
- }
380
-
381
- close(): Promise<void> {
382
- return (this.closing ??= (async () => {
383
- let drainFailure: unknown;
384
- for (;;) {
385
- try {
386
- await this.drainLive();
387
- } catch (error: unknown) {
388
- drainFailure = error;
389
- break;
390
- }
391
- await this.chain;
392
- if (this.buffered.length === 0) break;
393
- }
394
- await this.chain;
395
- const failures: Error[] = [];
396
- if (drainFailure !== undefined) {
397
- failures.push(
398
- drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)),
399
- );
400
- }
401
- this.persistence.tracker.release(this, this.materialized || this.everAppended);
402
- if (failures.length > 1)
403
- throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
404
- if (failures[0] !== undefined) throw failures[0];
405
- })());
406
- }
407
-
408
- [Symbol.asyncDispose](): Promise<void> {
409
- return this.close();
410
- }
411
-
412
- enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void {
413
- this.buffered.push(structuredClone(event));
414
- if (this.batchTimer !== undefined || this.drainPaused) return;
415
- this.batchTimer = setTimeout(() => {
416
- this.batchTimer = undefined;
417
- this.drainLive().catch(reportBackgroundFailure);
418
- }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
419
- }
420
-
421
- resetAfterRewind(cursor: number, inheritedEventCount?: number): void {
422
- this.cursor = cursor;
423
- if (inheritedEventCount !== undefined) {
424
- (this.state as { inheritedEventCount: SessionLogOffset }).inheritedEventCount =
425
- SessionLogOffset(inheritedEventCount);
426
- }
427
- }
428
-
429
- drainLive(): Promise<void> {
430
- return (this.draining ??= this.drainBuffered().finally(() => {
431
- this.draining = undefined;
432
- }));
433
- }
434
-
435
- private async drainBuffered(): Promise<void> {
436
- if (this.batchTimer !== undefined) {
437
- clearTimeout(this.batchTimer);
438
- this.batchTimer = undefined;
439
- }
440
- this.drainPaused = false;
441
- while (this.buffered.length > 0) {
442
- await this.enqueueChain(async () => {
443
- const batch = this.buffered.splice(0);
444
- try {
445
- const fresh = batch.filter((event) => event.seq >= this.cursor);
446
- if (fresh.length === 0) return;
447
- for (const [index, event] of fresh.entries()) {
448
- if (event.seq !== this.cursor + index) {
449
- throw new Error(
450
- `append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`,
451
- );
452
- }
453
- }
454
- await this.persistence.appendBatch(
455
- this.header,
456
- this.state.inheritedEventCount,
457
- fresh,
458
- this.tornTruncateTo,
459
- );
460
- this.tornTruncateTo = undefined;
461
- this.cursor += fresh.length;
462
- this.materialized = true;
463
- } catch (error: unknown) {
464
- this.buffered = batch.concat(this.buffered);
465
- this.drainPaused = true;
466
- throw error;
467
- }
468
- });
469
- }
470
- }
471
-
472
- private enqueueChain(op: () => Promise<void>): Promise<void> {
473
- const next = this.chain.then(op);
474
- this.chain = next.catch(() => {});
475
- return next;
476
- }
477
-
478
- private async run(operation: string, op: () => Promise<void>): Promise<void> {
479
- this.assertOpen(operation);
480
- return this.enqueueChain(async () => {
481
- this.assertOpen(operation);
482
- return op();
483
- });
484
- }
485
-
486
- private assertOpen(operation: string): void {
487
- if (this.closing !== undefined) throw new SessionHandleClosedError(this.id, operation);
488
- }
489
-
490
- static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
491
- }
492
-
493
- // 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
494
- // (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
495
- const localized = (text: { zh: string; en: string }): string => text as unknown as string;
496
-
497
- // 投影 checkpoint 的写回节流:两个分支共用。
498
- // 上游 session-query 的**冷会话观察缓存**容量(`preparedSessionCacheSize`):缓存按**条目数**持有完整
499
- // `Session`(含整份 append-only log),被活跃观察租约钉住的条目不淘汰;一次读取中途转成 live 时会重试 live
500
- // 路径,但**不会顺手清掉已缓存的 cold 条目**——所以同一个 id 可能同时有两份 log 驻留。上游默认 5;本部署把它
501
- // 收到最小的 1:本地单用户、rdb 后端,冷读复用的收益不值这份内存放大。上游校验是 >= 1(不能设 0)。
502
- const preparedSessionCacheSchema = z
503
- .number()
504
- .step(1)
505
- .min(1)
506
- .default(1)
507
- .description(
508
- localized({
509
- zh: "冷会话观察缓存容量(条数,最小 1):每个条目持一份完整会话日志;本部署默认 1,把同 id 两份日志的驻留压到最小。",
510
- 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.",
511
- }),
512
- );
513
-
514
- const projectionCacheSchema = z
515
- .object({
516
- writeEveryEvents: z
517
- .natural()
518
- .min(1)
519
- .default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS)
520
- .description(
521
- localized({
522
- zh: "每累计这么多事件写回一次投影 checkpoint。",
523
- en: "Write the projection checkpoint back after this many events.",
524
- }),
525
- ),
526
- writeIntervalMs: z
527
- .natural()
528
- .min(1)
529
- .default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
530
- .description(
531
- localized({
532
- zh: "距离上次写回超过这个间隔(毫秒)时也写一次。",
533
- en: "Also write back when this many ms passed since the last one.",
534
- }),
535
- ),
536
- })
537
- .default({
538
- writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
539
- writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS,
540
- });
541
-
542
- export class SessionPersistenceRdb extends SessionPersistence {
543
- static inject = ["sessions"];
544
-
545
- // 显式声明「输入 → 解析后」的形状:整段是 volatile,读它要过 `.get()`。union + volatile 的推断形状与手写类型
546
- // 在可选性 / 索引签名上不完全对齐,所以这里断言一次,类型面由此固定。
547
- static Config = z
548
- .union([
549
- z.object({
550
- type: z.const("sqlite"),
551
- path: z
552
- .string()
553
- .required()
554
- .description(
555
- localized({
556
- zh: "SQLite 数据库文件路径;`:memory:` 是内存库(进程结束即丢)。",
557
- en: "SQLite database file path; `:memory:` keeps it in memory and loses it with the process.",
558
- }),
559
- ),
560
- journalMode: z
561
- .union(["wal", "delete", "truncate", "persist"] as const)
562
- .default("wal")
563
- .description(
564
- localized({
565
- zh: "SQLite 日志模式;默认 wal(并发读最好)。",
566
- en: "SQLite journal mode; `wal` by default (best concurrent reads).",
567
- }),
568
- ),
569
- busyTimeout: z
570
- .number()
571
- .step(1)
572
- .min(0)
573
- .default(DEFAULT_BUSY_TIMEOUT_MS)
574
- .description(
575
- localized({
576
- zh: "写锁冲突时的忙等超时(毫秒)。",
577
- en: "Busy timeout in ms while a write lock is contended.",
578
- }),
579
- ),
580
- projectionCache: projectionCacheSchema,
581
- preparedSessionCacheSize: preparedSessionCacheSchema,
582
- }),
583
- z.object({
584
- type: z.const("postgres"),
585
- connectionString: z
586
- .string()
587
- .required()
588
- .description(
589
- localized({
590
- zh: "PostgreSQL 连接串(含库名与凭证)。",
591
- en: "PostgreSQL connection string (database and credentials).",
592
- }),
593
- ),
594
- schema: z
595
- .string()
596
- .default("public")
597
- .description(
598
- localized({
599
- zh: "库内的 schema 名;默认 public。",
600
- en: "Schema name inside the database; `public` by default.",
601
- }),
602
- ),
603
- projectionCache: projectionCacheSchema,
604
- preparedSessionCacheSize: preparedSessionCacheSchema,
605
- }),
606
- ])
607
- .description(
608
- localized({
609
- zh: "会话持久化后端:改这里的连接参数会在这一行重挂时生效(Loader 会重装被改的行)。",
610
- en: "Session persistence backend: edits apply when this row is remounted (the Loader re-installs changed rows).",
611
- }),
612
- )
613
- .volatile() as unknown as z<Config, ResolvedConfig>;
614
-
615
- override readonly name = "session-rdb";
616
-
617
- readonly tracker = new RdbBackendTracker(this.name);
618
-
619
- private readonly backend: Backend;
620
- private storeIdentity!: string;
621
- private readonly ready: Promise<void>;
622
-
623
- private readonly writeGuard = new WriteGuard();
624
-
625
- private readonly reuseEventIds = new Map<SessionId, Map<number, string>>();
626
-
627
- private readonly liveBuffers = new Map<SessionId, SessionEvent[]>();
628
- private readonly liveReady = new Map<SessionId, Promise<void>>();
629
-
630
- // 本次装配用的配置快照(整段 Config 是 volatile,重挂会给新的)。
631
- readonly config: Config;
632
-
633
- constructor(
634
- ctx: Context,
635
- config: ResolvedConfig,
636
-
637
- injectedBackend?: Backend,
638
- ) {
639
- super(ctx);
640
-
641
- // 配置就是这一行的 config(cordis.patch.yml / profile patch,或设置页改它);整段 Config 标了 volatile,
642
- // 生效靠 **Loader 重挂这一行**(settings 写完经 `reconcileProfilePatches` 重装受影响的 entry)——重挂构造
643
- // 新实例、拿到新快照,所以这里取一次就够,运行期不必反复读引用。
644
- this.config = config.get();
645
- this.backend = injectedBackend ?? createBackend(this.config);
646
- this.ready = this.init();
647
- this.installLiveRouting(ctx);
648
-
649
- new SessionBranchRdb(this.ctx);
650
-
651
- // 冷会话观察缓存的容量在这一行配置里(默认见 `preparedSessionCacheSchema`);session-query 自己不会再读它。
652
- const queryConfig =
653
- this.config.preparedSessionCacheSize === undefined
654
- ? {}
655
- : { preparedSessionCacheSize: this.config.preparedSessionCacheSize };
656
- this.ctx.plugin(SessionQueryRdb, queryConfig);
657
-
658
- registerSessionImport(this.ctx, this);
659
-
660
- registerSessionDeletion(this.ctx, this);
661
-
662
- registerSessionExport(this.ctx, this);
663
-
664
- registerSessionGc(this.ctx, this);
665
-
666
- registerSessionUsage(this.ctx, this);
667
-
668
- registerSessionRows(this.ctx, this.backend);
669
-
670
- installStorageTakeover(this.ctx, {
671
- repository: this.backend.storage,
672
- ready: this.ready,
673
- projectionCache: {
674
- writeEveryEvents:
675
- this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
676
- writeIntervalMs:
677
- this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS,
678
- },
679
- });
680
- }
681
-
682
- private async init(): Promise<void> {
683
- await this.backend.open();
684
- this.storeIdentity = this.backend.storeIdentity;
685
- }
686
-
687
- async create(
688
- header: SessionHeader,
689
- options?: SessionPersistenceCreateOptions,
690
- ): Promise<SessionHandle> {
691
- options?.signal?.throwIfAborted();
692
- const snapshot = materializeCreateHeader(header);
693
- if (snapshot.isSeeded && options?.inheritedEventCount === undefined) {
694
- throw new TypeError("seeded session metadata requires an inherited event count");
695
- }
696
- const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
697
- if (!snapshot.isSeeded && inheritedEventCount !== 0) {
698
- throw new TypeError("unseeded session metadata inherited event count must be 0");
699
- }
700
- await this.ready;
701
- options?.signal?.throwIfAborted();
702
- if (
703
- this.tracker.hasPending(snapshot.id) ||
704
- (await this.backend.getSession(snapshot.id)) !== undefined
705
- ) {
706
- throw new SessionAlreadyExistsError(snapshot.id);
707
- }
708
- this.tracker.registerCreated(snapshot, inheritedEventCount);
709
- return this.tracker.adopt(
710
- new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
711
- cursor: 0,
712
- materialized: false,
713
- inheritedEventCount,
714
- }),
715
- );
716
- }
717
-
718
- async open(
719
- id: SessionId,
720
- access: SessionAccess,
721
- options?: SessionPersistenceOpenOptions,
722
- ): Promise<SessionHandle> {
723
- options?.signal?.throwIfAborted();
724
- await this.ready;
725
- options?.signal?.throwIfAborted();
726
- const pending = this.tracker.pendingOf(id);
727
- if (access === "read") {
728
- if (pending !== undefined) {
729
- return this.tracker.adopt(
730
- new RdbSessionHandle(this, id, pending.header, "read", {
731
- cursor: 0,
732
- materialized: false,
733
- inheritedEventCount: pending.inheritedEventCount,
734
- }),
735
- );
736
- }
737
- const log = await this.readLog(id, {}, options?.signal);
738
- if (log === undefined) throw new SessionPersistenceNotFoundError(id);
739
-
740
- repairReadView(log.events);
741
-
742
- validateStoredEvents(log.meta, log.events);
743
- return this.tracker.adopt(
744
- new RdbSessionHandle(this, id, log.meta, "read", {
745
- cursor: log.events.length,
746
- materialized: true,
747
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
748
- }),
749
- );
750
- }
751
- this.tracker.claimWrite(id);
752
- try {
753
- if (pending !== undefined) {
754
- return this.tracker.adopt(
755
- new RdbSessionHandle(this, id, pending.header, "write", {
756
- cursor: pending.cursor,
757
- materialized: false,
758
- inheritedEventCount: pending.inheritedEventCount,
759
- }),
760
- );
761
- }
762
- const log = await this.readLog(id, {}, options?.signal);
763
- if (log === undefined) throw new SessionPersistenceNotFoundError(id);
764
- repairReadView(log.events);
765
- validateStoredEvents(log.meta, log.events);
766
-
767
- if (log.migrated && log.events.length !== log.storedCount) {
768
- await this.rewriteMigratedLog(id, log);
769
- }
770
-
771
- this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
772
- return this.tracker.adopt(
773
- new RdbSessionHandle(this, id, log.meta, "write", {
774
- cursor: log.events.length,
775
- materialized: true,
776
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
777
- ...(log.tornFrom !== undefined ? { tornTruncateTo: log.tornFrom } : {}),
778
- }),
779
- );
780
- } catch (error: unknown) {
781
- this.tracker.releaseClaim(id);
782
- throw error;
783
- }
784
- }
785
-
786
- flush(): Promise<void> {
787
- return this.tracker.flushAll();
788
- }
789
-
790
- async stat(
791
- id: SessionId,
792
- options?: SessionPersistenceStatOptions,
793
- ): Promise<SessionPersistenceSnapshot | undefined> {
794
- options?.signal?.throwIfAborted();
795
- await this.ready;
796
- options?.signal?.throwIfAborted();
797
- const pending = this.tracker.pendingOf(id);
798
- if (pending !== undefined) {
799
- return { header: pending.header, revision: pending.revision };
800
- }
801
- const row = await this.backend.getSession(id);
802
- if (row === undefined) return undefined;
803
- return {
804
- header: rowToMeta(row),
805
- revision: this.rowRevision(row),
806
- };
807
- }
808
-
809
- async list(
810
- options?: SessionPersistenceListOptions,
811
- ): Promise<readonly SessionPersistenceSnapshot[]> {
812
- const signal = options?.signal;
813
- const snapshots: SessionPersistenceSnapshot[] = [];
814
- const listed = new Set<SessionId>();
815
- for (const [id, pending] of this.tracker.pendingEntries()) {
816
- snapshots.push({ header: pending.header, revision: pending.revision });
817
- listed.add(id);
818
- }
819
- signal?.throwIfAborted();
820
- await this.ready;
821
- signal?.throwIfAborted();
822
- const rows = await this.backend.listSessions();
823
- signal?.throwIfAborted();
824
- for (const row of rows) {
825
- if (listed.has(row.fSessionId as SessionId)) continue;
826
- snapshots.push({ header: rowToMeta(row), revision: this.rowRevision(row) });
827
- }
828
- return snapshots;
829
- }
830
-
831
- async deleteSession(id: SessionId, signal?: AbortSignal): Promise<void> {
832
- signal?.throwIfAborted();
833
- await this.ready;
834
- signal?.throwIfAborted();
835
- const row = await this.backend.getSession(id);
836
- if (row === undefined) {
837
- throw new SessionDeletionError(`session "${id}" not found`, "SESSION_NOT_FOUND");
838
- }
839
- if (row.fArchivedAt === null) {
840
- throw new SessionDeletionError(
841
- `session "${id}" is not archived; only archived sessions can be deleted`,
842
- "SESSION_NOT_ARCHIVED",
843
- );
844
- }
845
- if (this.tracker.hasPending(id) || this.tracker.hasOpenHandle(id)) {
846
- throw new SessionDeletionError(
847
- `session "${id}" is live; stop it before deleting`,
848
- "SESSION_LIVE",
849
- );
850
- }
851
- await this.unarchiveBeforeDeletion(id);
852
- const referenced = await this.backend.transaction((tx) => tx.deleteSession(id));
853
- this.liveBuffers.delete(id);
854
- this.liveReady.delete(id);
855
- this.liveFailures.delete(id);
856
- this.liveDropWarned.delete(id);
857
- this.reuseEventIds.delete(id);
858
- await this.dropSessionStats(id);
859
- await this.refreshEventUsageFlags(referenced);
860
- }
861
-
862
- // GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。
863
- async collectOrphans(): Promise<number> {
864
- await this.ready;
865
- return this.backend.collectOrphans();
866
- }
867
-
868
- // GC 通道:回收父已不存在的 subagent 会话(live 的跳过),返回删除的会话数。
869
- async collectOrphanSessions(): Promise<number> {
870
- await this.ready;
871
- const orphans = await this.backend.listOrphanSubagentSessions();
872
- const deletable = orphans.filter(
873
- (id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id),
874
- );
875
- if (deletable.length === 0) return 0;
876
- const { deleted, eventIds } = await this.backend.transaction((tx) =>
877
- tx.deleteSessions(deletable),
878
- );
879
- for (const id of deletable) {
880
- this.liveBuffers.delete(id);
881
- this.liveReady.delete(id);
882
- this.liveFailures.delete(id);
883
- this.liveDropWarned.delete(id);
884
- this.reuseEventIds.delete(id);
885
- await this.dropSessionStats(id);
886
- }
887
- await this.refreshEventUsageFlags(eventIds);
888
- return deleted;
889
- }
890
-
891
- // 用量统计:SQL 聚合的按天 × 模型桶与按会话行(事件行去重、排除孤儿行)。
892
- async usageReport(sinceMs?: number): Promise<UsageAggregate> {
893
- await this.ready;
894
- return this.backend.usageReport(sinceMs);
895
- }
896
-
897
- // GC 通道:VACUUM;调用方需先停止运行中的写路径(见 `registerSessionGc`)。
898
- async vacuum(): Promise<void> {
899
- await this.ready;
900
- await this.backend.vacuum();
901
- }
902
-
903
- /**
904
- * 已归档的会话 id(`t_sessions.f_archived_at` 非空)。`session/list` 默认过滤掉它们。
905
- * 读的是持久化里的归档标记,不依赖 `workspaceRegistry` 服务的就绪时机。
906
- * @returns 归档过的 Session id 集。
907
- */
908
- async archivedSessionIds(): Promise<ReadonlySet<string>> {
909
- await this.ready;
910
- const state = await this.backend.storage.readWorkspaceState();
911
- return new Set((state?.archivedSessionIds ?? []).map(String));
912
- }
913
-
914
- private async unarchiveBeforeDeletion(id: SessionId): Promise<void> {
915
- const registry = this.ctx.get("workspaceRegistry") as unknown as
916
- | {
917
- readonly archivedSessionIds: readonly SessionId[];
918
- unarchiveSession(sessionId: SessionId): Promise<void>;
919
- }
920
- | undefined;
921
- if (registry === undefined) return;
922
- if (!registry.archivedSessionIds.includes(id)) return;
923
- await registry.unarchiveSession(id);
924
- }
925
-
926
- async readRaw(
927
- id: SessionId,
928
- signal?: AbortSignal,
929
- ): Promise<
930
- | { meta: SessionHeader; inheritedEventCount: number; filename: string; content: string }
931
- | undefined
932
- > {
933
- signal?.throwIfAborted();
934
- await this.ready;
935
- signal?.throwIfAborted();
936
- const log = await this.readLog(id, {}, signal);
937
- if (log === undefined) return undefined;
938
- repairReadView(log.events);
939
- // 整段日志导出:只丢弃「已不平衡」之后的尾部(尾部未闭合的 step 是中断运行的正常形状,由上游 resume 补 closers)
940
- const events = balanceRewindPrefix(log.events, { keepOpenTail: true });
941
- const inheritedEventCount = Math.min(log.inheritedEventCount, events.length);
942
- return {
943
- meta: log.meta,
944
- inheritedEventCount,
945
-
946
- filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
947
- content: toJsonlArtifact(log.meta, inheritedEventCount, events),
948
- };
949
- }
950
-
951
- async materializeEmpty(
952
- meta: SessionHeader,
953
- inheritedEventCount: SessionLogOffset,
954
- ): Promise<void> {
955
- await this.ready;
956
- await this.backend.transaction(async (tx) => {
957
- await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
958
- await tx.bumpRevision(meta.id);
959
- });
960
- this.tracker.materialized(meta.id);
961
- this.writeGuard.confirmHead(meta.id, -1);
962
- }
963
-
964
- async appendBatch(
965
- meta: SessionHeader,
966
- inheritedEventCount: SessionLogOffset,
967
- events: readonly SessionEvent[],
968
- tornTruncateTo?: number,
969
- ): Promise<boolean> {
970
- await this.ready;
971
- if (events.length === 0) return false;
972
-
973
- // 本仓库自造的事件类型落库前补上 `ignorable` 信封:不补的话上游那道校验连写入都拒。
974
- sealOwnEvents(events);
975
- validateStoredEvents(meta, [...events]);
976
-
977
- const reuse = this.reuseEventIds.get(meta.id);
978
- let confirmedHead = -1;
979
- // 本批用量行由 `appendEventTail` 折好;事务成功后旁路据此累加 token 汇总表。
980
- let usageRows: EventUsageRow[] = [];
981
- await this.backend.transaction(async (tx) => {
982
- if (tornTruncateTo !== undefined) {
983
- await tx.deleteBridgeTail(meta.id, tornTruncateTo);
984
- const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
985
- if (prev === undefined) {
986
- await tx.updateHead(meta.id, "", -1);
987
- } else {
988
- await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
989
- }
990
- }
991
- await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
992
- const head = await tx.getHead(meta.id);
993
-
994
- this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
995
- const appended = await appendEventTail(
996
- tx,
997
- meta,
998
- events,
999
- { parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
1000
- reuse,
1001
- );
1002
- usageRows = appended.usageRows;
1003
- await tx.updateHead(meta.id, appended.headEventId, appended.headSequence);
1004
- await tx.bumpRevision(meta.id, maxEventTime(events));
1005
- confirmedHead = appended.headSequence;
1006
- });
1007
- // 事务**成功之后**才丢掉复用映射:失败(并发写者校验、唯一键冲突等)时这份映射还没被消费,
1008
- // 重试同一个 append 还要靠它复用父会话的事件行——提前删掉会让前缀事件行被重新插入一遍。
1009
- if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
1010
-
1011
- this.writeGuard.confirmHead(meta.id, confirmedHead);
1012
- this.tracker.materialized(meta.id);
1013
- await this.recordSessionStats(meta.id, events, usageRows);
1014
- return true;
1015
- }
1016
-
1017
- async readLog(
1018
- id: SessionId,
1019
- options: { fromSeq?: number } = {},
1020
- signal?: AbortSignal,
1021
- ): Promise<
1022
- | {
1023
- meta: SessionHeader;
1024
-
1025
- inheritedEventCount: number;
1026
- events: SessionEvent[];
1027
- tornFrom?: number;
1028
-
1029
- incarnation: string;
1030
-
1031
- revision: number;
1032
-
1033
- storedCount: number;
1034
-
1035
- migrated: boolean;
1036
- }
1037
- | undefined
1038
- > {
1039
- signal?.throwIfAborted();
1040
- await this.ready;
1041
- signal?.throwIfAborted();
1042
- const row = await this.backend.getSession(id);
1043
- if (row === undefined) return undefined;
1044
- const meta = rowToMeta(row);
1045
- const eventRows =
1046
- options.fromSeq === undefined
1047
- ? await this.backend.getEventRows(id)
1048
- : await this.backend.getEventRows(id, options.fromSeq);
1049
- signal?.throwIfAborted();
1050
-
1051
- if (isLegacyVersion(row.fVersion)) {
1052
- try {
1053
- const converted = convertLegacyRows(row, eventRows);
1054
- return {
1055
- meta: converted.meta,
1056
- inheritedEventCount: converted.inheritedEventCount,
1057
- events: converted.events,
1058
- incarnation: row.fIncarnation,
1059
- revision: row.fRevision,
1060
- storedCount: eventRows.length,
1061
- migrated: true,
1062
- };
1063
- } catch (error: unknown) {
1064
- this.ctx.logger.warn(
1065
- `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)})`,
1066
- );
1067
- const adopted = adoptLegacyRows(row, eventRows);
1068
- return {
1069
- meta: adopted.meta,
1070
- inheritedEventCount: adopted.inheritedEventCount,
1071
- events: adopted.events,
1072
- incarnation: row.fIncarnation,
1073
- revision: row.fRevision,
1074
- storedCount: eventRows.length,
1075
- migrated: false,
1076
- ...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
1077
- };
1078
- }
1079
- }
1080
- const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
1081
- // 版本号不完全可信:写路径曾把回退视图的结果以当前版本号落库,于是库里存在「v4 标记 + 旧代形状」
1082
- // 的行——在扫干净的边界之内再按内容判一次(撕裂尾部与坏行已经被 `scanRows` 丢掉了)。
1083
- if (needsShapeAdoption(preserved)) {
1084
- const adopted = adoptLegacyRows(row, eventRows);
1085
- return {
1086
- meta: adopted.meta,
1087
- inheritedEventCount: adopted.inheritedEventCount,
1088
- events: adopted.events,
1089
- incarnation: row.fIncarnation,
1090
- revision: row.fRevision,
1091
- storedCount: eventRows.length,
1092
- migrated: false,
1093
- ...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
1094
- };
1095
- }
1096
- return {
1097
- meta,
1098
- inheritedEventCount: row.fSeedLength ?? 0,
1099
- events: preserved,
1100
- incarnation: row.fIncarnation,
1101
- revision: row.fRevision,
1102
- storedCount: eventRows.length,
1103
- migrated: false,
1104
- ...(tornFrom !== undefined ? { tornFrom } : {}),
1105
- };
1106
- }
1107
-
1108
- // 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
1109
- // 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
1110
- // token 走写路径已折好的用量行,活动计数走本批事件类型。
1111
- private async recordSessionStats(
1112
- id: SessionId,
1113
- events: readonly SessionEvent[],
1114
- usageRows: readonly EventUsageRow[],
1115
- ): Promise<void> {
1116
- const usageBuckets = sessionUsageBuckets(usageRows);
1117
- if (usageBuckets.length > 0) {
1118
- try {
1119
- await this.backend.incrementSessionUsage(id, usageBuckets);
1120
- } catch (error: unknown) {
1121
- this.ctx.logger.warn(
1122
- `session-rdb: token usage for "${id}" not updated (${describeError(error)})`,
1123
- );
1124
- }
1125
- }
1126
- const countBuckets = sessionCountBuckets(events);
1127
- if (countBuckets.length > 0) {
1128
- try {
1129
- await this.backend.incrementSessionCounts(id, countBuckets);
1130
- } catch (error: unknown) {
1131
- this.ctx.logger.warn(
1132
- `session-rdb: activity counts for "${id}" not updated (${describeError(error)})`,
1133
- );
1134
- }
1135
- }
1136
- }
1137
-
1138
- // 重算一个会话的两张汇总表(旧日志重写后;best-effort,同 `recordSessionStats`)——
1139
- // fork 与 rewind 都不动统计。
1140
- async rebuildSessionStats(id: SessionId): Promise<void> {
1141
- try {
1142
- await this.backend.rebuildSessionStats(id);
1143
- } catch (error: unknown) {
1144
- this.ctx.logger.warn(
1145
- `session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`,
1146
- );
1147
- }
1148
- }
1149
-
1150
- // 重算这些用量行的引用标记(会话删除 / 日志重写之后:消失的引用要让孤儿行退出统计)。
1151
- private async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
1152
- const unique = [...new Set(eventIds)];
1153
- if (unique.length === 0) return;
1154
- try {
1155
- await this.backend.refreshEventUsageFlags(unique);
1156
- } catch (error: unknown) {
1157
- this.ctx.logger.warn(
1158
- `session-rdb: usage reference flags not refreshed (${describeError(error)})`,
1159
- );
1160
- }
1161
- }
1162
-
1163
- // 会话删除时清掉它的汇总行(引用标记由调用方在拿到被删事件后一并重算)。
1164
- private async dropSessionStats(id: SessionId): Promise<void> {
1165
- try {
1166
- await this.backend.deleteSessionStats(id);
1167
- } catch (error: unknown) {
1168
- this.ctx.logger.warn(
1169
- `session-rdb: session stats for "${id}" not deleted (${describeError(error)})`,
1170
- );
1171
- }
1172
- }
1173
-
1174
- private async rewriteMigratedLog(
1175
- id: SessionId,
1176
- log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
1177
- ): Promise<void> {
1178
- let removed: readonly string[] = [];
1179
- await this.backend.transaction(async (tx) => {
1180
- // 重写会删光该会话的桥接行再重建,先做并发写者校验(与 `appendBatch` 同一个理由):否则另一实例在
1181
- // open 前 / 期间提交的事件会被静默丢掉;调用方刚读过这份日志,guard 没有该会话时记下当前磁盘 head。
1182
- const head = await tx.getHead(id);
1183
- if (!this.writeGuard.has(id)) this.writeGuard.confirmHead(id, head.fHeadSequence);
1184
- this.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
1185
- removed = await tx.deleteBridgeTail(id, 0);
1186
- await tx.upsertSession(
1187
- { meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
1188
- randomUUID(),
1189
- );
1190
- const appended = await appendEventTail(tx, log.meta, log.events, {
1191
- parentId: "",
1192
- nextSeq: 0,
1193
- });
1194
- await tx.updateHead(id, appended.headEventId, appended.headSequence);
1195
- await tx.bumpRevision(id, maxEventTime(log.events));
1196
- });
1197
- // 重写换了事件行:被删掉的那批成了孤儿(引用标记要落回 0),本会话汇总按新事件重算。
1198
- await this.rebuildSessionStats(id);
1199
- await this.refreshEventUsageFlags(removed);
1200
- }
1201
-
1202
- async listSnapshots(
1203
- signal?: AbortSignal,
1204
- ): Promise<Array<SessionPersistenceSnapshot & { inheritedEventCount: number }>> {
1205
- signal?.throwIfAborted();
1206
- await this.ready;
1207
- signal?.throwIfAborted();
1208
- const snapshots: Array<SessionPersistenceSnapshot & { inheritedEventCount: number }> = [];
1209
- const listed = new Set<SessionId>();
1210
- for (const [id, pending] of this.tracker.pendingEntries()) {
1211
- snapshots.push({
1212
- header: pending.header,
1213
- revision: pending.revision,
1214
- inheritedEventCount: pending.inheritedEventCount,
1215
- });
1216
- listed.add(id);
1217
- }
1218
- const rows = await this.backend.listSessions();
1219
- signal?.throwIfAborted();
1220
- for (const row of rows) {
1221
- if (listed.has(row.fSessionId as SessionId)) continue;
1222
- snapshots.push({
1223
- header: rowToMeta(row),
1224
- revision: this.rowRevision(row),
1225
- inheritedEventCount: row.fSeedLength ?? 0,
1226
- });
1227
- }
1228
- return snapshots;
1229
- }
1230
-
1231
- async readStoredRevision(
1232
- id: SessionId,
1233
- signal?: AbortSignal,
1234
- ): Promise<SessionPersistenceRevision | undefined> {
1235
- signal?.throwIfAborted();
1236
- await this.ready;
1237
- signal?.throwIfAborted();
1238
- const row = await this.backend.getSession(id);
1239
- if (row === undefined) return undefined;
1240
- return this.rowRevision(row);
1241
- }
1242
-
1243
- async createAndAppend(
1244
- header: SessionHeader,
1245
- events: readonly SessionEvent[],
1246
- inheritedEventCount?: number,
1247
- ): Promise<void> {
1248
- const handle = await this.create(
1249
- header,
1250
- inheritedEventCount === undefined
1251
- ? undefined
1252
- : { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
1253
- );
1254
- try {
1255
- if (events.length > 0) await handle.append(events);
1256
- } finally {
1257
- await handle.close();
1258
- }
1259
- }
1260
-
1261
- async load(
1262
- id: SessionId,
1263
- signal?: AbortSignal,
1264
- ): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
1265
- signal?.throwIfAborted();
1266
- await this.ready;
1267
- signal?.throwIfAborted();
1268
- // 一条读取路径:load 的语义是「读一次完整快照」,不经 handle 面再读一遍。
1269
- const log = await this.readLog(id, {}, signal);
1270
- if (log === undefined) {
1271
- // 未落库的 live 会话没有磁盘视图:与 handle.read 的 detached 口径一致,返回空日志。
1272
- const pending = this.tracker.pendingOf(id);
1273
- if (pending !== undefined) {
1274
- return {
1275
- meta: pending.header,
1276
- inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
1277
- events: [],
1278
- };
1279
- }
1280
- throw new SessionPersistenceNotFoundError(id);
1281
- }
1282
- repairReadView(log.events);
1283
- validateStoredEvents(log.meta, log.events);
1284
- return {
1285
- meta: log.meta,
1286
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1287
- events: log.events,
1288
- };
1289
- }
1290
-
1291
- async append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
1292
- const handle = await this.open(id, "write");
1293
- try {
1294
- await handle.append(events);
1295
- } finally {
1296
- await handle.close();
1297
- }
1298
- }
1299
-
1300
- async readFrom(
1301
- id: SessionId,
1302
- fromSeq: number,
1303
- signal?: AbortSignal,
1304
- ): Promise<{
1305
- meta: SessionHeader;
1306
- inheritedEventCount: number;
1307
- events: readonly SessionEvent[];
1308
- }> {
1309
- const log = await this.readLog(id, { fromSeq }, signal);
1310
- if (log === undefined) throw new SessionPersistenceNotFoundError(id);
1311
- return {
1312
- meta: log.meta,
1313
- inheritedEventCount: log.inheritedEventCount,
1314
- events: log.events,
1315
- };
1316
- }
1317
-
1318
- async close(): Promise<void> {
1319
- await this.ready;
1320
- await this.backend.close();
1321
- }
1322
-
1323
- registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void {
1324
- this.reuseEventIds.set(childId, new Map(map));
1325
- }
1326
-
1327
- // 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。
1328
- dropReuseEventIds(childId: SessionId): void {
1329
- this.reuseEventIds.delete(childId);
1330
- }
1331
-
1332
- internals(): SessionPersistenceRdbInternals {
1333
- return {
1334
- backend: this.backend,
1335
- writeGuard: this.writeGuard,
1336
- create: (meta, inheritedEventCount) =>
1337
- this.create(
1338
- meta,
1339
- inheritedEventCount === undefined
1340
- ? undefined
1341
- : { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
1342
- ),
1343
- append: (id, events) => this.append(id, events),
1344
- load: (id) => this.load(id),
1345
- inspect: (id, signal) => this.load(id, signal),
1346
- readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
1347
- listSnapshots: (signal) => this.listSnapshots(signal),
1348
- readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
1349
- registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
1350
- dropReuseEventIds: (childId) => this.dropReuseEventIds(childId),
1351
- };
1352
- }
1353
-
1354
- private rowRevision(row: import("./backend.ts").SessionRow): SessionPersistenceRevision {
1355
- return SessionPersistenceRevision(
1356
- `${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`,
1357
- );
1358
- }
1359
-
1360
- private readonly liveFailures = new Map<SessionId, Error>();
1361
- private readonly liveDropWarned = new Set<SessionId>();
1362
-
1363
- // live 会话的持久化初始化失败是**终态**(cwd / 继承前缀冲突、id 碰撞、后端打不开都不自愈):
1364
- // 记账一条 error、丢掉已缓冲的事件,之后该会话的事件不再进内存缓冲(否则表现为
1365
- // 「聊了一整轮、重启后全丢,内存里还留一份无界副本」),flush 把失败抛回上游。
1366
- private watchLiveReady(ctx: Context, session: Session, ready: Promise<void>): void {
1367
- void ready.catch((error: unknown) => {
1368
- const failure =
1369
- error instanceof Error
1370
- ? error
1371
- : new Error(
1372
- typeof error === "string" ? error : `live session "${session.id}" persistence failed`,
1373
- );
1374
- this.liveFailures.set(session.id, failure);
1375
- this.liveBuffers.delete(session.id);
1376
- ctx.logger.error(
1377
- `session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`,
1378
- );
1379
- });
1380
- }
1381
-
1382
- private installLiveRouting(ctx: Context): void {
1383
- ctx.on("session/created", (session: Session) => {
1384
- this.liveBuffers.set(session.id, []);
1385
- const ready = this.ensureLiveHandle(session);
1386
- this.liveReady.set(session.id, ready);
1387
- this.watchLiveReady(ctx, session, ready);
1388
- });
1389
-
1390
- for (const session of ctx.sessions.list()) {
1391
- this.liveBuffers.set(session.id, []);
1392
- const ready = this.ensureLiveHandle(session);
1393
- this.liveReady.set(session.id, ready);
1394
- this.watchLiveReady(ctx, session, ready);
1395
- }
1396
- ctx.on("session/event", (session: Session, event: SessionEvent) => {
1397
- const handle = this.tracker.writerOf(session.id);
1398
- if (handle !== undefined) {
1399
- handle.enqueueLive(event, (error) => {
1400
- ctx.logger.warn(
1401
- `session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`,
1402
- );
1403
- });
1404
- return;
1405
- }
1406
- if (this.liveFailures.has(session.id)) {
1407
- if (!this.liveDropWarned.has(session.id)) {
1408
- this.liveDropWarned.add(session.id);
1409
- ctx.logger.warn(
1410
- `session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`,
1411
- );
1412
- }
1413
- return;
1414
- }
1415
- this.liveBuffers.get(session.id)?.push(structuredClone(event));
1416
- });
1417
- ctx.on("session/flush", (session: Session) => {
1418
- const failure = this.liveFailures.get(session.id);
1419
- if (failure !== undefined) return Promise.reject(failure);
1420
- const handle = this.tracker.writerOf(session.id);
1421
- if (handle === undefined) {
1422
- const ready = this.liveReady.get(session.id);
1423
- if (ready === undefined) return undefined;
1424
- return ready.then(() => {
1425
- const settled = this.tracker.writerOf(session.id);
1426
- if (settled === undefined) return undefined;
1427
- return settled.drainLive().then(() => settled.flush());
1428
- });
1429
- }
1430
- return handle.drainLive().then(() => handle.flush());
1431
- });
1432
- ctx.on("session/disposed", (session: Session) => {
1433
- const ready = this.liveReady.get(session.id);
1434
- this.liveBuffers.delete(session.id);
1435
- this.liveReady.delete(session.id);
1436
- this.liveFailures.delete(session.id);
1437
- this.liveDropWarned.delete(session.id);
1438
- const closeHandle = (): void => {
1439
- const handle = this.tracker.writerOf(session.id);
1440
- if (handle === undefined) return;
1441
- handle.close().catch((error: unknown) => {
1442
- ctx.logger.warn(
1443
- `session-rdb: final drain for session "${session.id}" failed: ${String(error)}`,
1444
- );
1445
- });
1446
- };
1447
- if (ready === undefined) {
1448
- closeHandle();
1449
- return;
1450
- }
1451
-
1452
- void ready.then(closeHandle, closeHandle);
1453
- });
1454
- ctx.effect(
1455
- () => async () => {
1456
- await Promise.allSettled(this.liveReady.values());
1457
- this.liveBuffers.clear();
1458
- this.liveReady.clear();
1459
- this.liveFailures.clear();
1460
- this.liveDropWarned.clear();
1461
- await this.tracker.closeAll();
1462
-
1463
- await this.close();
1464
- },
1465
- `${this.name} open handles`,
1466
- );
1467
- }
1468
-
1469
- private async ensureLiveHandle(session: Session): Promise<void> {
1470
- const id = session.header.id;
1471
- if (this.tracker.writerOf(id) !== undefined) return;
1472
- await this.ready;
1473
- const stored = await this.readLog(id, {});
1474
- let handle: RdbSessionHandle;
1475
- if (stored === undefined) {
1476
- handle = (await this.create(session.header, {
1477
- inheritedEventCount: session.inheritedEventCount,
1478
- })) as RdbSessionHandle;
1479
- const seed = liveEventsOf(session);
1480
- if (seed.length > 0) await handle.append(seed);
1481
- } else {
1482
- if (stored.meta.cwd !== session.header.cwd) {
1483
- throw new Error(
1484
- `session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`,
1485
- );
1486
- }
1487
- if (stored.inheritedEventCount !== session.inheritedEventCount) {
1488
- throw new Error(
1489
- `session "${id}" is already persisted with a different inherited event count (id collision)`,
1490
- );
1491
- }
1492
- assertVersion(stored.meta);
1493
-
1494
- repairReadView(stored.events);
1495
- const seed = liveEventsOf(session);
1496
- if (!seedCoversPrefix(seed, stored.events)) {
1497
- throw new Error(
1498
- `session "${id}" already has a persisted log on disk that does not match this live session (id collision)`,
1499
- );
1500
- }
1501
- handle = (await this.open(id, "write")) as RdbSessionHandle;
1502
-
1503
- const suffix = seed.slice(stored.events.length);
1504
- if (suffix.length > 0) await handle.append(suffix);
1505
- }
1506
-
1507
- const buffered = this.liveBuffers.get(id);
1508
- if (buffered !== undefined && buffered.length > 0) {
1509
- this.liveBuffers.set(id, []);
1510
- for (const event of buffered) {
1511
- handle.enqueueLive(event, () => {});
1512
- }
1513
- }
1514
- }
1515
- }
1516
-
1517
- function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly SessionEvent[]): boolean {
1518
- return (
1519
- prefix.length <= seed.length &&
1520
- prefix.every((event, index) => {
1521
- const seedEvent = seed[index];
1522
- return seedEvent !== undefined && JSON.stringify(seedEvent) === JSON.stringify(event);
1523
- })
1524
- );
1525
- }
1526
-
1527
- // 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。
1528
- function sessionCountBuckets(events: readonly SessionEvent[]): SessionCountBucket[] {
1529
- const counted = new Map<string, SessionCountBucket>();
1530
- for (const event of events) {
1531
- if (!(COUNTED_EVENT_TYPES as readonly string[]).includes(event.type)) continue;
1532
- const day = localDayKey(event.time);
1533
- const bucket = counted.get(day) ?? { day, turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
1534
- addActivityCount(bucket, event.type, 1);
1535
- counted.set(day, bucket);
1536
- }
1537
- return [...counted.values()];
1538
- }
1539
-
1540
- // 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。
1541
- function sessionUsageBuckets(usageRows: readonly EventUsageRow[]): SessionUsageBucket[] {
1542
- const counted = new Map<string, SessionUsageBucket>();
1543
- for (const row of usageRows) {
1544
- const provider = row.fProvider ?? "";
1545
- const model = row.fModel ?? "";
1546
- const key = `${row.fDay}#${provider}#${model}`;
1547
- const bucket = counted.get(key) ?? {
1548
- day: row.fDay,
1549
- provider,
1550
- model,
1551
- inputTokens: 0,
1552
- outputTokens: 0,
1553
- cacheReadTokens: 0,
1554
- reasoningTokens: 0,
1555
- totalTokens: 0,
1556
- };
1557
- bucket.inputTokens += row.fInputTokens;
1558
- bucket.outputTokens += row.fOutputTokens;
1559
- bucket.cacheReadTokens += row.fCacheReadTokens;
1560
- bucket.reasoningTokens += row.fReasoningTokens;
1561
- bucket.totalTokens += row.fTotalTokens;
1562
- counted.set(key, bucket);
1563
- }
1564
- return [...counted.values()];
1565
- }
1566
-
1567
- function describeError(error: unknown): string {
1568
- return error instanceof Error ? error.message : "unknown error";
1569
- }
1570
-
1571
- function createBackend(config: Config): Backend {
1572
- if (config.type === "sqlite") {
1573
- return new SqliteBackend({
1574
- path: config.path,
1575
- journalMode: config.journalMode ?? "wal",
1576
- busyTimeout: config.busyTimeout ?? DEFAULT_BUSY_TIMEOUT_MS,
1577
- });
1578
- }
1579
- const pool = new Pool({ connectionString: config.connectionString });
1580
-
1581
- pool.on("error", () => {});
1582
- const db = drizzlePg({ client: pool });
1583
- const identityBase = [
1584
- "postgres",
1585
- pool.options.host ?? "localhost",
1586
- String(pool.options.port ?? 5432),
1587
- pool.options.database ?? "",
1588
- config.schema ?? "public",
1589
- ].join(":");
1590
- return new PostgresBackend(db, {
1591
- identityBase,
1592
- schema: config.schema ?? "public",
1593
- close: () => pool.end(),
1594
- });
1595
- }
1596
-
1597
- async function appendEventTail(
1598
- tx: BackendTx,
1599
- meta: SessionHeader,
1600
- events: readonly SessionEvent[],
1601
- anchor: { parentId: string; nextSeq: number },
1602
- reuse?: ReadonlyMap<number, string>,
1603
- ): Promise<{ headEventId: string; headSequence: number; usageRows: EventUsageRow[] }> {
1604
- let parentId = anchor.parentId;
1605
- let nextSeq = anchor.nextSeq;
1606
-
1607
- const eventRows: EventInsert[] = [];
1608
- const usageRows: EventUsageRow[] = [];
1609
- const bridgeRows: Array<{
1610
- fSessionId: SessionId;
1611
- fEventId: string;
1612
- fSequence: number;
1613
- fSurfaceOp: string | null;
1614
- }> = [];
1615
- for (const event of events) {
1616
- const reusedId = reuse?.get(event.seq);
1617
- const eventId = reusedId ?? randomUUID();
1618
- if (reusedId === undefined) {
1619
- const { kind, role, name, actionId } = eventDimensions(event);
1620
-
1621
- const raw = event as SessionEvent & {
1622
- ignorable?: unknown;
1623
- surfaceOp?: unknown;
1624
- sourceEventSeqs?: unknown;
1625
- };
1626
- const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = raw;
1627
- const row: EventInsert = {
1628
- fEventId: eventId,
1629
- fParentId: parentId,
1630
- fType: event.type,
1631
- fKind: kind,
1632
- fRole: role,
1633
- fName: name,
1634
- fActionId: actionId,
1635
- fEncoding: EVENT_ENCODING,
1636
- fData: JSON.stringify({ ...envelope, data }),
1637
- fCreatedAt: event.time,
1638
- };
1639
- eventRows.push(row);
1640
- // 用量行在这里一次折好(读侧维度一起物化),后端只负责插入,事务后旁路再据此累加汇总表。
1641
- const usageRow = usageRowOf(row, meta.origin === "subagent");
1642
- if (usageRow !== undefined) usageRows.push(usageRow);
1643
- }
1644
- const surfaceOp =
1645
- (event as SessionEvent<SurfaceEventType>).surfaceOp === undefined
1646
- ? null
1647
- : JSON.stringify((event as SessionEvent<SurfaceEventType>).surfaceOp);
1648
- bridgeRows.push({
1649
- fSessionId: meta.id,
1650
- fEventId: eventId,
1651
- fSequence: nextSeq,
1652
- fSurfaceOp: surfaceOp,
1653
- });
1654
- parentId = eventId;
1655
- nextSeq++;
1656
- }
1657
- if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
1658
- await tx.insertBridges(bridgeRows);
1659
-
1660
- if (events.some((event) => (event.type as string) === "session/title")) {
1661
- await tx.refreshTitle(meta.id);
1662
- }
1663
- return { headEventId: parentId, headSequence: nextSeq - 1, usageRows };
1664
- }
1665
-
1666
- export default SessionPersistenceRdb;
1
+ export {
2
+ SessionPersistenceRdb,
3
+ type Config,
4
+ type ResolvedConfig,
5
+ } from "./session-store.ts";
6
+ export { default } from "./session-store.ts";
7
+ export { SCHEMA_VERSION } from "./version.ts";
8
+ export { SESSION_ROWS_PATH, type SessionRowsItem, type SessionRowsValue } from "./rows.ts";
9
+ export {
10
+ SessionBranchRdb,
11
+ SessionBranchRdbProvider,
12
+ liveEventsOf,
13
+ locateTurnEnd,
14
+ } from "./branch/index.ts";
15
+ export { SessionDeletionError, type SessionDeletionErrorCode } from "./deletion-error.ts";