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

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 +32 -36
  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
@@ -0,0 +1,286 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ SessionLogOffset,
4
+ type SessionEvent,
5
+ type SurfaceEventType,
6
+ type SessionHeader,
7
+ type SessionId,
8
+ } from "@deepseek-ai/dsh-session";
9
+ import { validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
10
+ import type { Backend, BackendTx, EventInsert } from "./backend.ts";
11
+ import type { WriteGuard } from "./write-guard.ts";
12
+ import type { RdbBackendTracker } from "./handles.ts";
13
+ import { EVENT_ENCODING } from "./version.ts";
14
+ import { eventDimensions } from "./event-shape.ts";
15
+ import { usageRowOf, type EventUsageRow } from "./log/usage-row.ts";
16
+ import { sealOwnEvents } from "./log/repair.ts";
17
+ import { sessionCountBuckets, sessionUsageBuckets } from "./stats-fold.ts";
18
+
19
+ function maxEventTime(events: readonly { readonly time?: number }[]): number | undefined {
20
+ let max: number | undefined;
21
+ for (const event of events) {
22
+ const time = event.time;
23
+ if (typeof time !== "number") continue;
24
+ if (max === undefined || time > max) max = time;
25
+ }
26
+ return max;
27
+ }
28
+
29
+ function describeError(error: unknown): string {
30
+ return error instanceof Error ? error.message : "unknown error";
31
+ }
32
+
33
+ export interface WritePathDeps {
34
+ readonly ready: Promise<void>;
35
+ backend(): Backend;
36
+ readonly tracker: RdbBackendTracker;
37
+ readonly writeGuard: WriteGuard;
38
+ readonly reuseEventIds: Map<SessionId, Map<number, string>>;
39
+ readonly logger: { warn(message: string): void };
40
+ readLog(id: SessionId): Promise<
41
+ | { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] }
42
+ | undefined
43
+ >;
44
+ }
45
+
46
+ /**
47
+ * 写侧:空会话物化、批量追加、统计旁路累加与重算、迁移后重写。
48
+ *
49
+ * 事务边界都在这里;`session-store.ts` 只负责装配与契约转发。
50
+ */
51
+ export function createWritePath(deps: WritePathDeps) {
52
+ return {
53
+ async materializeEmpty(
54
+ meta: SessionHeader,
55
+ inheritedEventCount: SessionLogOffset,
56
+ ): Promise<void> {
57
+ await deps.ready;
58
+ await deps.backend().transaction(async (tx) => {
59
+ await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
60
+ await tx.bumpRevision(meta.id);
61
+ });
62
+ deps.tracker.materialized(meta.id);
63
+ deps.writeGuard.confirmHead(meta.id, -1);
64
+ },
65
+
66
+ async appendBatch(
67
+ meta: SessionHeader,
68
+ inheritedEventCount: SessionLogOffset,
69
+ events: readonly SessionEvent[],
70
+ tornTruncateTo?: number,
71
+ ): Promise<boolean> {
72
+ await deps.ready;
73
+ if (events.length === 0) return false;
74
+
75
+ // 本仓库自造的事件类型落库前补上 `ignorable` 信封:不补的话上游那道校验连写入都拒。
76
+ sealOwnEvents(events);
77
+ validateStoredEvents(meta, [...events]);
78
+
79
+ const reuse = deps.reuseEventIds.get(meta.id);
80
+ let confirmedHead = -1;
81
+ // 本批用量行由 `appendEventTail` 折好;事务成功后旁路据此累加 token 汇总表。
82
+ let usageRows: EventUsageRow[] = [];
83
+ await deps.backend().transaction(async (tx) => {
84
+ if (tornTruncateTo !== undefined) {
85
+ await tx.deleteBridgeTail(meta.id, tornTruncateTo);
86
+ const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
87
+ if (prev === undefined) {
88
+ await tx.updateHead(meta.id, "", -1);
89
+ } else {
90
+ await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
91
+ }
92
+ }
93
+ await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
94
+ const head = await tx.getHead(meta.id);
95
+
96
+ deps.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
97
+ const appended = await appendEventTail(
98
+ tx,
99
+ meta,
100
+ events,
101
+ { parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
102
+ reuse,
103
+ );
104
+ usageRows = appended.usageRows;
105
+ await tx.updateHeadAndRevision(
106
+ meta.id,
107
+ appended.headEventId,
108
+ appended.headSequence,
109
+ maxEventTime(events),
110
+ );
111
+ confirmedHead = appended.headSequence;
112
+ });
113
+ // 事务**成功之后**才丢掉复用映射:失败(并发写者校验、唯一键冲突等)时这份映射还没被消费,
114
+ // 重试同一个 append 还要靠它复用父会话的事件行——提前删掉会让前缀事件行被重新插入一遍。
115
+ if (reuse !== undefined) deps.reuseEventIds.delete(meta.id);
116
+
117
+ deps.writeGuard.confirmHead(meta.id, confirmedHead);
118
+ deps.tracker.materialized(meta.id);
119
+ await this.recordSessionStats(meta.id, events, usageRows);
120
+ return true;
121
+ },
122
+
123
+
124
+ async recordSessionStats(
125
+ id: SessionId,
126
+ events: readonly SessionEvent[],
127
+ usageRows: readonly EventUsageRow[],
128
+ ): Promise<void> {
129
+ const usageBuckets = sessionUsageBuckets(usageRows);
130
+ if (usageBuckets.length > 0) {
131
+ try {
132
+ await deps.backend().incrementSessionUsage(id, usageBuckets);
133
+ } catch (error: unknown) {
134
+ deps.logger.warn(
135
+ `session-rdb: token usage for "${id}" not updated (${describeError(error)})`,
136
+ );
137
+ }
138
+ }
139
+ const countBuckets = sessionCountBuckets(events);
140
+ if (countBuckets.length > 0) {
141
+ try {
142
+ await deps.backend().incrementSessionCounts(id, countBuckets);
143
+ } catch (error: unknown) {
144
+ deps.logger.warn(
145
+ `session-rdb: activity counts for "${id}" not updated (${describeError(error)})`,
146
+ );
147
+ }
148
+ }
149
+ },
150
+
151
+ // 重算一个会话的两张汇总表(旧日志重写后;best-effort,同 `recordSessionStats`)——
152
+ // fork 与 rewind 都不动统计。
153
+ async rebuildSessionStats(id: SessionId): Promise<void> {
154
+ await deps.ready;
155
+ try {
156
+ await deps.backend().rebuildSessionStats(id);
157
+ } catch (error: unknown) {
158
+ deps.logger.warn(
159
+ `session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`,
160
+ );
161
+ }
162
+ },
163
+
164
+ // 重算这些用量行的引用标记(会话删除 / 日志重写之后:消失的引用要让孤儿行退出统计)。
165
+ async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
166
+ const unique = [...new Set(eventIds)];
167
+ if (unique.length === 0) return;
168
+ try {
169
+ await deps.backend().refreshEventUsageFlags(unique);
170
+ } catch (error: unknown) {
171
+ deps.logger.warn(
172
+ `session-rdb: usage reference flags not refreshed (${describeError(error)})`,
173
+ );
174
+ }
175
+ },
176
+
177
+ // 会话删除时清掉它的汇总行(引用标记由调用方在拿到被删事件后一并重算)。
178
+ async dropSessionStats(id: SessionId): Promise<void> {
179
+ try {
180
+ await deps.backend().deleteSessionStats(id);
181
+ } catch (error: unknown) {
182
+ deps.logger.warn(
183
+ `session-rdb: session stats for "${id}" not deleted (${describeError(error)})`,
184
+ );
185
+ }
186
+ },
187
+
188
+ async rewriteMigratedLog(
189
+ id: SessionId,
190
+ log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
191
+ ): Promise<void> {
192
+ let removed: readonly string[] = [];
193
+ await deps.backend().transaction(async (tx) => {
194
+ // 重写会删光该会话的桥接行再重建,先做并发写者校验(与 `appendBatch` 同一个理由):否则另一实例在
195
+ // open 前 / 期间提交的事件会被静默丢掉;调用方刚读过这份日志,guard 没有该会话时记下当前磁盘 head。
196
+ const head = await tx.getHead(id);
197
+ if (!deps.writeGuard.has(id)) deps.writeGuard.confirmHead(id, head.fHeadSequence);
198
+ deps.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
199
+ removed = await tx.deleteBridgeTail(id, 0);
200
+ await tx.upsertSession(
201
+ { meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
202
+ randomUUID(),
203
+ );
204
+ const appended = await appendEventTail(tx, log.meta, log.events, {
205
+ parentId: "",
206
+ nextSeq: 0,
207
+ });
208
+ await tx.updateHead(id, appended.headEventId, appended.headSequence);
209
+ await tx.bumpRevision(id, maxEventTime(log.events));
210
+ });
211
+ // 重写换了事件行:被删掉的那批成了孤儿(引用标记要落回 0),本会话汇总按新事件重算。
212
+ await this.rebuildSessionStats(id);
213
+ await this.refreshEventUsageFlags(removed);
214
+ },
215
+
216
+ };
217
+ }
218
+
219
+ async function appendEventTail(
220
+ tx: BackendTx,
221
+ meta: SessionHeader,
222
+ events: readonly SessionEvent[],
223
+ anchor: { parentId: string; nextSeq: number },
224
+ reuse?: ReadonlyMap<number, string>,
225
+ ): Promise<{ headEventId: string; headSequence: number; usageRows: EventUsageRow[] }> {
226
+ let parentId = anchor.parentId;
227
+ let nextSeq = anchor.nextSeq;
228
+
229
+ const eventRows: EventInsert[] = [];
230
+ const usageRows: EventUsageRow[] = [];
231
+ const bridgeRows: Array<{
232
+ fSessionId: SessionId;
233
+ fEventId: string;
234
+ fSequence: number;
235
+ fSurfaceOp: string | null;
236
+ }> = [];
237
+ for (const event of events) {
238
+ const reusedId = reuse?.get(event.seq);
239
+ const eventId = reusedId ?? randomUUID();
240
+ if (reusedId === undefined) {
241
+ const { kind, role, name, actionId } = eventDimensions(event);
242
+
243
+ const raw = event as SessionEvent & {
244
+ ignorable?: unknown;
245
+ surfaceOp?: unknown;
246
+ sourceEventSeqs?: unknown;
247
+ };
248
+ const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = raw;
249
+ const row: EventInsert = {
250
+ fEventId: eventId,
251
+ fParentId: parentId,
252
+ fType: event.type,
253
+ fKind: kind,
254
+ fRole: role,
255
+ fName: name,
256
+ fActionId: actionId,
257
+ fEncoding: EVENT_ENCODING,
258
+ fData: JSON.stringify({ ...envelope, data }),
259
+ fCreatedAt: event.time,
260
+ };
261
+ eventRows.push(row);
262
+ // 用量行在这里一次折好(读侧维度一起物化),后端只负责插入,事务后旁路再据此累加汇总表。
263
+ const usageRow = usageRowOf(row, meta.origin === "subagent");
264
+ if (usageRow !== undefined) usageRows.push(usageRow);
265
+ }
266
+ const surfaceOp =
267
+ (event as SessionEvent<SurfaceEventType>).surfaceOp === undefined
268
+ ? null
269
+ : JSON.stringify((event as SessionEvent<SurfaceEventType>).surfaceOp);
270
+ bridgeRows.push({
271
+ fSessionId: meta.id,
272
+ fEventId: eventId,
273
+ fSequence: nextSeq,
274
+ fSurfaceOp: surfaceOp,
275
+ });
276
+ parentId = eventId;
277
+ nextSeq++;
278
+ }
279
+ if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
280
+ await tx.insertBridges(bridgeRows);
281
+
282
+ if (events.some((event) => (event.type as string) === "session/title")) {
283
+ await tx.refreshTitle(meta.id);
284
+ }
285
+ return { headEventId: parentId, headSequence: nextSeq - 1, usageRows };
286
+ }
@@ -1,401 +0,0 @@
1
- import { d as repairRequestHeaders, h as scanRows, i as normalizeToCurrentShape, m as rowToMeta } from "./log-CcWy46VE.mjs";
2
- import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
3
- import { createSessionFormatCatalogWithChildren } from "@deepseek-ai/dsh-session-format-catalog";
4
- import { randomUUID } from "node:crypto";
5
- import { SessionBranch, SessionBranchError, balanceRewindPrefix, rewindKeepLength } from "@morlay/session-branch";
6
- // src/legacy.ts
7
- function physicalHeader(row) {
8
- const common = {
9
- type: "session",
10
- version: row.fVersion,
11
- id: row.fSessionId,
12
- createdAt: row.fCreatedAt,
13
- ...row.fCwd !== null ? { cwd: row.fCwd } : {},
14
- ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
15
- ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
16
- delegationDepth: row.fDelegationDepth ?? 0,
17
- ...row.fAgentPreset !== null ? { agentPreset: row.fAgentPreset } : {}
18
- };
19
- return row.fVersion < 2 ? {
20
- ...common,
21
- ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
22
- } : {
23
- ...common,
24
- isSeeded: row.fSeedLength !== null
25
- };
26
- }
27
- function physicalEvent(row) {
28
- const stored = JSON.parse(row.fData);
29
- const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
30
- return {
31
- type: row.fType,
32
- seq: row.fSequence,
33
- time: row.fCreatedAt,
34
- data: full ? stored["data"] : stored,
35
- ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
36
- };
37
- }
38
- const restoreCatalog = createSessionFormatCatalogWithChildren([]);
39
- function convertLegacyRows(row, eventRows) {
40
- const restore = restoreCatalog.createRestore(physicalHeader(row), {
41
- recovery: "strict",
42
- validation: "transformed"
43
- });
44
- for (const eventRow of eventRows) restore.decodeRow(physicalEvent(eventRow));
45
- const artifact = restore.finish();
46
- return {
47
- meta: artifact.header,
48
- inheritedEventCount: artifact.inheritedEventCount,
49
- events: artifact.events
50
- };
51
- }
52
- function isLegacyVersion(version) {
53
- return version < SESSION_FORMAT_VERSION;
54
- }
55
- function adoptLegacyRows(row, eventRows) {
56
- const { preserved, tornFrom } = scanRows(eventRows, 0);
57
- repairRequestHeaders(preserved);
58
- normalizeToCurrentShape(preserved);
59
- return {
60
- meta: {
61
- ...rowToMeta(row),
62
- version: SESSION_FORMAT_VERSION
63
- },
64
- inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
65
- events: preserved,
66
- ...tornFrom !== void 0 ? { tornFrom } : {}
67
- };
68
- }
69
- // src/branch.ts
70
- function assertRewindBoundary(id, toBoundary, boundaryType, head) {
71
- if (toBoundary === -1) return;
72
- if (toBoundary > head) throw new SessionBranchError(`rewind boundary ${toBoundary} is beyond the stored head ${head}`, "INVALID_BOUNDARY");
73
- if (boundaryType === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
74
- if (boundaryType !== "turn/end" && boundaryType !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryType})`, "INVALID_BOUNDARY");
75
- }
76
- function locateTurnEnd(events, atSeq, mode = "after") {
77
- const ends = events.filter((event) => event.type === "turn/end").map((event) => event.seq);
78
- if (atSeq === void 0) {
79
- const last = ends.at(-1);
80
- if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
81
- return last;
82
- }
83
- if (mode === "before") {
84
- let boundary = -1;
85
- for (const seq of ends) if (seq < atSeq) boundary = seq;
86
- else break;
87
- return boundary;
88
- }
89
- const firstAfter = ends.find((seq) => seq >= atSeq);
90
- if (firstAfter !== void 0) return firstAfter;
91
- const lastStart = [...events].reverse().find((event) => event.type === "turn/start");
92
- if (lastStart !== void 0 && lastStart.seq <= atSeq) throw new SessionBranchError(`anchor ${atSeq} lies inside an open turn`, "OPEN_TURN");
93
- const last = ends.at(-1);
94
- if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
95
- return last;
96
- }
97
- function renumber(events, offset) {
98
- return events.map((event, index) => ({
99
- ...event,
100
- seq: offset + index
101
- }));
102
- }
103
- function mintSessionId() {
104
- return `session-${randomUUID()}`;
105
- }
106
- function resetSurfaceManager(surfaceManager) {
107
- const state = surfaceManager._state;
108
- state.nodes = [];
109
- state.replaceGeneration = 0;
110
- state.contentGeneration = 0;
111
- state.projectedMessages = /* @__PURE__ */ new Map();
112
- surfaceManager._lastProcessedSeq = surfaceManager.baseSeq - 1;
113
- surfaceManager._pendingPlan = void 0;
114
- }
115
- /**
116
- * 会话**当前内存 log** 的快照(复制一份)。用于"需要完整序列"的操作(首次落库的 seed、rewind / fork 的历史),
117
- * 不走已弃用的同步历史读 `snapshotEvents()`——那三个读口上游已声明「新调用禁止」,而这里要的是当前内存里
118
- * 那份完整日志,不是按需分页的历史。
119
- * @param session - 会话。
120
- * @returns 事件副本(调用方可以自由遍历)。
121
- */
122
- function liveEventsOf(session) {
123
- return [...session.log];
124
- }
125
- /** @returns 截断后日志里最后一个 `turn/start` 的 turn 号(没有则 0)。 */
126
- function truncateLiveSession(session, newLength) {
127
- const s = session;
128
- const lastTurn = s.log.slice(0, newLength).findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
129
- s.log.length = newLength;
130
- s.eventsSnapshot = void 0;
131
- s.headerFold = void 0;
132
- s.headerFoldSeq = 0;
133
- s.contextFold = void 0;
134
- s.contextFoldSeq = 0;
135
- s.derived = [];
136
- s.derivedNodes = 0;
137
- s.derivedGeneration = 0;
138
- resetSurfaceManager(s.surfaceManager);
139
- return lastTurn;
140
- }
141
- function replaceLiveSessionLog(session, events) {
142
- const s = session;
143
- s.log.length = 0;
144
- s.log.push(...events);
145
- s.eventsSnapshot = void 0;
146
- s.headerFold = void 0;
147
- s.headerFoldSeq = 0;
148
- s.contextFold = void 0;
149
- s.contextFoldSeq = 0;
150
- s.derived = [];
151
- s.derivedNodes = 0;
152
- s.derivedGeneration = 0;
153
- resetSurfaceManager(s.surfaceManager);
154
- }
155
- var SessionBranchRdbProvider = class {
156
- persistence;
157
- live;
158
- name = "session-rdb";
159
- constructor(persistence, live = {
160
- getSession: () => void 0,
161
- getAgent: () => void 0,
162
- flush: async () => true
163
- }) {
164
- this.persistence = persistence;
165
- this.live = live;
166
- }
167
- async readBranchPrefix(id, atSeq, mode = "after", signal) {
168
- const { events } = await this.readRawEvents(id, signal);
169
- const boundary = locateTurnEnd(events, atSeq, mode);
170
- return {
171
- seq: boundary,
172
- events: events.slice(0, boundary + 1)
173
- };
174
- }
175
- async readRawEvents(id, signal) {
176
- const stored = await this.persistence.readLog(id, {}, signal);
177
- if (stored === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
178
- return {
179
- meta: stored.meta,
180
- events: stored.events
181
- };
182
- }
183
- async forkFrom(sourceId, options = {}, signal) {
184
- signal?.throwIfAborted();
185
- const { atSeq, anchorMode = "after", seedSuffix = [], childSessionId, meta = {} } = options;
186
- const source = await this.persistence.readLog(sourceId, {}, signal);
187
- if (source === void 0) throw new SessionBranchError(`session "${sourceId}" not found`, "SESSION_NOT_FOUND");
188
- const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
189
- const prefix = balanceRewindPrefix(source.events.slice(0, boundary + 1));
190
- if (prefix.length <= boundary) this.live.warn?.(`session-rdb: fork "${sourceId}" dropped ${boundary + 1 - prefix.length} trailing event(s) from seq ${prefix.length} to keep the seed's step pairs balanced`);
191
- const childId = childSessionId ?? mintSessionId();
192
- const childMeta = {
193
- version: SESSION_FORMAT_VERSION,
194
- id: childId,
195
- createdAt: meta.createdAt ?? Date.now(),
196
- ...meta.cwd !== void 0 ? { cwd: meta.cwd } : source.meta.cwd !== void 0 ? { cwd: source.meta.cwd } : {},
197
- parentSession: sourceId,
198
- isSeeded: true,
199
- ...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : source.meta.agentPreset !== void 0 ? { agentPreset: source.meta.agentPreset } : {},
200
- ...meta.origin !== void 0 ? { origin: meta.origin } : {},
201
- ...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {}
202
- };
203
- const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
204
- const internals = this.persistence.internals();
205
- const reusable = !source.migrated && source.events.length === source.storedCount;
206
- const reuse = /* @__PURE__ */ new Map();
207
- if (reusable) {
208
- const sourceRows = await internals.backend.getEventRows(sourceId);
209
- const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
210
- for (const event of prefix) {
211
- const eventId = sourceEventIds.get(event.seq);
212
- if (eventId !== void 0) reuse.set(event.seq, eventId);
213
- }
214
- } else if (prefix.length > 0) this.live.warn?.(`session-rdb: fork "${sourceId}" copies event rows instead of reusing them — its read view comes from a migration, so stored seqs need not line up`);
215
- internals.registerReuseEventIds(childId, reuse);
216
- try {
217
- const handle = await this.persistence.create(childMeta, { inheritedEventCount: SessionLogOffset(prefix.length) });
218
- try {
219
- if (seed.length > 0) await handle.append(seed);
220
- } finally {
221
- await handle.close();
222
- }
223
- } catch (error) {
224
- internals.dropReuseEventIds(childId);
225
- throw error;
226
- }
227
- return childId;
228
- }
229
- async rewind(id, toBoundary, signal) {
230
- signal?.throwIfAborted();
231
- if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
232
- const live = this.live.getSession(id);
233
- if (live !== void 0) await this.live.flush(live);
234
- const internals = this.persistence.internals();
235
- const row = await internals.backend.getSession(id);
236
- if (row === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
237
- const meta = rowToMeta(row);
238
- let rawKeepLength;
239
- let kept;
240
- if (isLegacyVersion(row.fVersion)) {
241
- const log = await this.persistence.readLog(id, {}, signal);
242
- if (log === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
243
- const boundaryType = log.events[toBoundary]?.type;
244
- assertRewindBoundary(id, toBoundary, boundaryType, log.events.length - 1);
245
- rawKeepLength = toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
246
- kept = balanceRewindPrefix(log.events.slice(0, rawKeepLength));
247
- } else {
248
- const boundaryType = toBoundary === -1 ? void 0 : await internals.backend.getEventTypeAt(id, toBoundary);
249
- assertRewindBoundary(id, toBoundary, boundaryType, row.fHeadSequence);
250
- rawKeepLength = toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
251
- }
252
- const keepLength = kept === void 0 ? await this.rewindKeepLength(id, rawKeepLength, internals.backend) : kept.length;
253
- if (keepLength < rawKeepLength) this.live.warn?.(`session-rdb: rewind "${id}" dropped ${rawKeepLength - keepLength} trailing event(s) from seq ${keepLength} to keep the retained prefix's step pairs balanced`);
254
- const denseBoundary = keepLength - 1;
255
- const newSeedLength = await internals.backend.transaction(async (tx) => {
256
- signal?.throwIfAborted();
257
- const head = await tx.getHead(id);
258
- if (denseBoundary > head.fHeadSequence) throw new SessionBranchError(`rewind boundary ${toBoundary} is beyond the stored head ${head.fHeadSequence}`, "INVALID_BOUNDARY");
259
- if (denseBoundary < head.fHeadSequence) {
260
- await tx.deleteBridgeTail(id, denseBoundary + 1);
261
- const prev = denseBoundary === -1 ? void 0 : await tx.getPrevBridge(id, denseBoundary);
262
- if (prev === void 0) await tx.updateHead(id, "", -1);
263
- else await tx.updateHead(id, prev.fEventId, prev.fSequence);
264
- }
265
- const storedSeedLength = await tx.getSeedLength(id);
266
- let shrunk = storedSeedLength;
267
- if (storedSeedLength !== null && storedSeedLength > denseBoundary + 1) {
268
- await tx.updateSeedLength(id, denseBoundary + 1);
269
- shrunk = denseBoundary + 1;
270
- }
271
- await tx.refreshTitle(id);
272
- await tx.bumpRevision(id);
273
- return shrunk;
274
- });
275
- internals.writeGuard.confirmHead(id, denseBoundary);
276
- if (live !== void 0) {
277
- const lastTurn = truncateLiveSession(live, keepLength);
278
- this.live.resetProjections?.(live);
279
- this.live.resetTokenMeter?.(live);
280
- const agent = this.live.getAgent(id);
281
- if (agent !== void 0) {
282
- agent.requestHeaderLogged = false;
283
- const phase = agent.phase;
284
- if (phase !== void 0) phase.lastTurn = lastTurn;
285
- }
286
- const handle = this.persistence.tracker.writerOf(id);
287
- if (handle !== void 0) handle.resetAfterRewind(keepLength, newSeedLength === null ? void 0 : newSeedLength);
288
- agent?.inbox?.clear();
289
- await this.live.flush(live);
290
- await this.refreshProjectionCache(live);
291
- } else await this.refreshProjectionCache({
292
- id,
293
- header: meta,
294
- inheritedEventCount: SessionLogOffset(newSeedLength ?? row.fSeedLength ?? 0),
295
- headSeq: keepLength - 1,
296
- snapshotEvents: () => []
297
- });
298
- return {
299
- header: rowToMeta(row),
300
- revision: await internals.readStoredRevision(id)
301
- };
302
- }
303
- async rewindKeepLength(id, rawKeepLength, backend) {
304
- if (rawKeepLength === 0) return 0;
305
- let limit = 64;
306
- for (;;) {
307
- const types = [...await backend.getEventTypesBefore(id, rawKeepLength, limit)].reverse().map((row) => row.fType);
308
- const windowStart = rawKeepLength - types.length;
309
- if (types.includes("turn/end") || windowStart === 0 || types.length >= rawKeepLength) return rewindKeepLength(types, rawKeepLength);
310
- limit *= 4;
311
- }
312
- }
313
- async refreshProjectionCache(session) {
314
- if (this.live.refreshProjectionCache === void 0) return;
315
- try {
316
- await this.live.refreshProjectionCache(session);
317
- } catch {}
318
- }
319
- };
320
- var SessionBranchRdb = class extends SessionBranch {
321
- static inject = ["sessionPersistence", "sessions"];
322
- warned = /* @__PURE__ */ new Set();
323
- constructor(ctx) {
324
- super(ctx);
325
- }
326
- resetLiveDerivedState(session) {
327
- this.resetProjectionCells(session);
328
- this.resetTokenMeterFold(session);
329
- }
330
- warnOnce(key, message) {
331
- if (this.warned.has(key)) return;
332
- this.warned.add(key);
333
- this.ctx.logger.warn(message);
334
- }
335
- resetProjectionCells(session) {
336
- const registrations = this.ctx.get("sessionProjections")?.registrations;
337
- if (!(registrations instanceof Map)) {
338
- this.warnOnce("sessionProjections.registrations", `session-rdb: ctx.sessionProjections.registrations is not a Map (upstream field shape changed); rewind leaves the projection cell caches of "${session.id}" stale, so replayed events can be skipped`);
339
- return;
340
- }
341
- for (const registration of registrations.values()) {
342
- const cells = registration?.cells;
343
- if (!(cells instanceof WeakMap)) {
344
- this.warnOnce("sessionProjections.cells", `session-rdb: ctx.sessionProjections registration cells are not a WeakMap (upstream field shape changed); rewind leaves the projection cell caches of "${session.id}" stale, so replayed events can be skipped`);
345
- continue;
346
- }
347
- cells.delete(session);
348
- }
349
- }
350
- resetTokenMeterFold(session) {
351
- const meter = this.ctx.get("tokenMeter");
352
- if (meter === void 0) {
353
- this.warnOnce("tokenMeter", `session-rdb: ctx.tokenMeter is not mounted; after rewind the token-meter fold watermark of "${session.id}" may stay stale, so compaction can report "step/end ... has no matching step/start event"`);
354
- return;
355
- }
356
- const states = meter.states;
357
- if (!(states instanceof WeakMap)) {
358
- this.warnOnce("tokenMeter.states", `session-rdb: ctx.tokenMeter.states is not a WeakMap (upstream field shape changed); after rewind the token-meter fold watermark of "${session.id}" may stay stale, so compaction can report "step/end ... has no matching step/start event"`);
359
- return;
360
- }
361
- states.delete(session);
362
- }
363
- provider = new SessionBranchRdbProvider(this.ctx.sessionPersistence, {
364
- getSession: (id) => this.ctx.sessions.get(id),
365
- getAgent: (id) => {
366
- return this.ctx.get("agents")?.get(id);
367
- },
368
- flush: (session) => this.ctx.sessions.flush(session),
369
- warn: (message) => {
370
- this.ctx.logger.warn(message);
371
- },
372
- resetProjections: (session) => this.resetProjectionCells(session),
373
- resetTokenMeter: (session) => this.resetTokenMeterFold(session),
374
- refreshProjectionCache: async (session) => {
375
- const cache = this.ctx.get("sessionProjectionCache");
376
- if (cache === void 0) return;
377
- try {
378
- if (session.headSeq !== void 0 && cache.truncateTo !== void 0) {
379
- await cache.truncateTo(session.header, session.inheritedEventCount, session.headSeq);
380
- return;
381
- }
382
- await cache.write(session);
383
- } catch (error) {
384
- this.ctx.logger.warn(`session-rdb: projection cache refresh after rewind for "${session.id}" failed (cache stays stale): ${String(error)}`);
385
- }
386
- }
387
- });
388
- readBranchPrefix(id, atSeq, mode, signal) {
389
- return this.provider.readBranchPrefix(id, atSeq, mode, signal);
390
- }
391
- readRawEvents(id, signal) {
392
- return this.provider.readRawEvents(id, signal);
393
- }
394
- forkFrom(sourceId, options, signal) {
395
- return this.provider.forkFrom(sourceId, options, signal);
396
- }
397
- rewind(id, toBoundary, signal) {
398
- return this.provider.rewind(id, toBoundary, signal);
399
- }
400
- };
401
- export { replaceLiveSessionLog as a, convertLegacyRows as c, locateTurnEnd as i, isLegacyVersion as l, SessionBranchRdbProvider as n, truncateLiveSession as o, liveEventsOf as r, adoptLegacyRows as s, SessionBranchRdb as t, restoreCatalog as u };