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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
  5. package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +28 -11
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1631
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +1 -26
  19. package/dist/testing.mjs +10503 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +28 -29
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +1 -4
  67. package/src/usage.ts +191 -0
  68. package/dist/index-D55G9n4k.d.mts +0 -271
  69. package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
@@ -1,8 +1,76 @@
1
- import { d as rowToMeta } from "./log-DO69NQnn.mjs";
1
+ import { c as repairRequestHeaders, d as rowToMeta, f as scanRows } from "./log-CnYct2Dv.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
- import { SessionBranch, SessionBranchError, balanceRewindPrefix, buildTimeline } from "@morlay/session-branch";
4
+ import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
5
+ import { SessionBranch, SessionBranchError, balanceRewindPrefix, buildTimeline, rewindKeepLength } from "@morlay/session-branch";
6
+ //#region 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
+ };
18
+ return row.fVersion < 2 ? {
19
+ ...common,
20
+ ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
21
+ } : {
22
+ ...common,
23
+ isSeeded: row.fSeedLength !== null
24
+ };
25
+ }
26
+ function physicalEvent(row) {
27
+ const stored = JSON.parse(row.fData);
28
+ const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
29
+ return {
30
+ type: row.fType,
31
+ seq: row.fSequence,
32
+ time: row.fCreatedAt,
33
+ data: full ? stored["data"] : stored,
34
+ ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
35
+ };
36
+ }
37
+ function convertLegacyRows(row, eventRows) {
38
+ const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
39
+ recovery: "strict",
40
+ validation: "transformed"
41
+ });
42
+ for (const eventRow of eventRows) restore.decodeRow(physicalEvent(eventRow));
43
+ const artifact = restore.finish();
44
+ return {
45
+ meta: artifact.header,
46
+ inheritedEventCount: artifact.inheritedEventCount,
47
+ events: artifact.events
48
+ };
49
+ }
50
+ function isLegacyVersion(version) {
51
+ return version < SESSION_FORMAT_VERSION;
52
+ }
53
+ function adoptLegacyRows(row, eventRows) {
54
+ const { preserved, tornFrom } = scanRows(eventRows, 0);
55
+ repairRequestHeaders(preserved);
56
+ return {
57
+ meta: {
58
+ ...rowToMeta(row),
59
+ version: SESSION_FORMAT_VERSION
60
+ },
61
+ inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
62
+ events: preserved,
63
+ ...tornFrom !== void 0 ? { tornFrom } : {}
64
+ };
65
+ }
66
+ //#endregion
5
67
  //#region src/branch.ts
68
+ function assertRewindBoundary(id, toBoundary, boundaryType, head) {
69
+ if (toBoundary === -1) return;
70
+ if (toBoundary > head) throw new SessionBranchError(`rewind boundary ${toBoundary} is beyond the stored head ${head}`, "INVALID_BOUNDARY");
71
+ if (boundaryType === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
72
+ if (boundaryType !== "turn/end" && boundaryType !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryType})`, "INVALID_BOUNDARY");
73
+ }
6
74
  function locateTurnEnd(events, atSeq, mode = "after") {
7
75
  const ends = events.filter((event) => event.type === "turn/end").map((event) => event.seq);
8
76
  if (atSeq === void 0) {
@@ -33,6 +101,15 @@ function renumber(events, offset) {
33
101
  function mintSessionId() {
34
102
  return `session-${randomUUID()}`;
35
103
  }
104
+ function resetSurfaceManager(surfaceManager) {
105
+ const state = surfaceManager._state;
106
+ state.nodes = [];
107
+ state.replaceGeneration = 0;
108
+ state.contentGeneration = 0;
109
+ state.projectedMessages = /* @__PURE__ */ new Map();
110
+ surfaceManager._lastProcessedSeq = surfaceManager.baseSeq - 1;
111
+ surfaceManager._pendingPlan = void 0;
112
+ }
36
113
  function truncateLiveSession(session, newLength) {
37
114
  const s = session;
38
115
  s.log.length = newLength;
@@ -44,12 +121,7 @@ function truncateLiveSession(session, newLength) {
44
121
  s.derived = [];
45
122
  s.derivedNodes = 0;
46
123
  s.derivedGeneration = 0;
47
- s.surfaceManager._state = {
48
- nodes: [],
49
- replaceGeneration: 0
50
- };
51
- s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
52
- s.surfaceManager._pendingPlan = void 0;
124
+ resetSurfaceManager(s.surfaceManager);
53
125
  }
54
126
  function replaceLiveSessionLog(session, events) {
55
127
  const s = session;
@@ -63,12 +135,7 @@ function replaceLiveSessionLog(session, events) {
63
135
  s.derived = [];
64
136
  s.derivedNodes = 0;
65
137
  s.derivedGeneration = 0;
66
- s.surfaceManager._state = {
67
- nodes: [],
68
- replaceGeneration: 0
69
- };
70
- s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
71
- s.surfaceManager._pendingPlan = void 0;
138
+ resetSurfaceManager(s.surfaceManager);
72
139
  }
73
140
  var SessionBranchRdbProvider = class {
74
141
  persistence;
@@ -104,7 +171,8 @@ var SessionBranchRdbProvider = class {
104
171
  const source = await this.persistence.readLog(sourceId, {}, signal);
105
172
  if (source === void 0) throw new SessionBranchError(`session "${sourceId}" not found`, "SESSION_NOT_FOUND");
106
173
  const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
107
- const prefix = source.events.slice(0, boundary + 1);
174
+ const prefix = balanceRewindPrefix(source.events.slice(0, boundary + 1));
175
+ 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`);
108
176
  const childId = childSessionId ?? mintSessionId();
109
177
  const childMeta = {
110
178
  version: SESSION_FORMAT_VERSION,
@@ -140,18 +208,26 @@ var SessionBranchRdbProvider = class {
140
208
  if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
141
209
  const live = this.live.getSession(id);
142
210
  if (live !== void 0) await this.live.flush(live);
143
- const raw = live === void 0 ? await this.persistence.readLog(id, {}, signal) : void 0;
144
- if (live === void 0 && raw === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
145
- const inspection = live === void 0 ? void 0 : await this.persistence.internals().inspect(id, signal);
146
- const events = live === void 0 ? raw.events : inspection.events;
147
- const meta = live === void 0 ? raw.meta : inspection.meta;
148
- const boundaryEvent = events[toBoundary];
149
- if (toBoundary === -1) {} else if (boundaryEvent === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
150
- else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`, "INVALID_BOUNDARY");
151
- const rawKeepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
152
- const kept = balanceRewindPrefix(events.slice(0, rawKeepLength));
153
- const keepLength = kept.length;
154
211
  const internals = this.persistence.internals();
212
+ const row = await internals.backend.getSession(id);
213
+ if (row === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
214
+ const meta = rowToMeta(row);
215
+ let rawKeepLength;
216
+ let kept;
217
+ if (isLegacyVersion(row.fVersion)) {
218
+ const log = await this.persistence.readLog(id, {}, signal);
219
+ if (log === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
220
+ const boundaryType = log.events[toBoundary]?.type;
221
+ assertRewindBoundary(id, toBoundary, boundaryType, log.events.length - 1);
222
+ rawKeepLength = toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
223
+ kept = balanceRewindPrefix(log.events.slice(0, rawKeepLength));
224
+ } else {
225
+ const boundaryType = toBoundary === -1 ? void 0 : await internals.backend.getEventTypeAt(id, toBoundary);
226
+ assertRewindBoundary(id, toBoundary, boundaryType, row.fHeadSequence);
227
+ rawKeepLength = toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
228
+ }
229
+ const keepLength = kept === void 0 ? await this.rewindKeepLength(id, rawKeepLength, internals.backend) : kept.length;
230
+ 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`);
155
231
  const denseBoundary = keepLength - 1;
156
232
  const newSeedLength = await internals.backend.transaction(async (tx) => {
157
233
  signal?.throwIfAborted();
@@ -177,6 +253,7 @@ var SessionBranchRdbProvider = class {
177
253
  if (live !== void 0) {
178
254
  truncateLiveSession(live, keepLength);
179
255
  this.live.resetProjections?.(live);
256
+ this.live.resetTokenMeter?.(live);
180
257
  const agent = this.live.getAgent(id);
181
258
  if (agent !== void 0) {
182
259
  agent.requestHeaderLogged = false;
@@ -192,33 +269,25 @@ var SessionBranchRdbProvider = class {
192
269
  } else await this.refreshProjectionCache({
193
270
  id,
194
271
  header: meta,
195
- inheritedEventCount: SessionLogOffset(raw.inheritedEventCount),
196
- snapshotEvents: () => kept
272
+ inheritedEventCount: SessionLogOffset(newSeedLength ?? row.fSeedLength ?? 0),
273
+ headSeq: keepLength - 1,
274
+ snapshotEvents: () => []
197
275
  });
198
- const row = await internals.backend.getSession(id);
199
- if (row === void 0) return {
200
- header: {
201
- version: SESSION_FORMAT_VERSION,
202
- id,
203
- createdAt: meta.createdAt,
204
- ...meta.cwd !== void 0 ? { cwd: meta.cwd } : {},
205
- ...meta.parentSession !== void 0 ? { parentSession: meta.parentSession } : {},
206
- isSeeded: meta.isSeeded,
207
- ...meta.origin !== void 0 ? { origin: meta.origin } : {},
208
- ...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {},
209
- ...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : {}
210
- },
211
- revision: await internals.readStoredRevision(id) ?? await this.persistence.readStoredRevision(id)
212
- };
213
276
  return {
214
277
  header: rowToMeta(row),
215
278
  revision: await internals.readStoredRevision(id)
216
279
  };
217
280
  }
218
- /**
219
- * 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
220
- * 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
221
- */
281
+ async rewindKeepLength(id, rawKeepLength, backend) {
282
+ if (rawKeepLength === 0) return 0;
283
+ let limit = 64;
284
+ for (;;) {
285
+ const types = [...await backend.getEventTypesBefore(id, rawKeepLength, limit)].reverse().map((row) => row.fType);
286
+ const windowStart = rawKeepLength - types.length;
287
+ if (types.includes("turn/end") || windowStart === 0 || types.length >= rawKeepLength) return rewindKeepLength(types, rawKeepLength);
288
+ limit *= 4;
289
+ }
290
+ }
222
291
  async refreshProjectionCache(session) {
223
292
  if (this.live.refreshProjectionCache === void 0) return;
224
293
  try {
@@ -228,24 +297,66 @@ var SessionBranchRdbProvider = class {
228
297
  };
229
298
  var SessionBranchRdb = class extends SessionBranch {
230
299
  static inject = ["sessionPersistence", "sessions"];
300
+ warned = /* @__PURE__ */ new Set();
231
301
  constructor(ctx) {
232
302
  super(ctx);
233
303
  }
304
+ resetLiveDerivedState(session) {
305
+ this.resetProjectionCells(session);
306
+ this.resetTokenMeterFold(session);
307
+ }
308
+ warnOnce(key, message) {
309
+ if (this.warned.has(key)) return;
310
+ this.warned.add(key);
311
+ this.ctx.logger.warn(message);
312
+ }
313
+ resetProjectionCells(session) {
314
+ const registrations = this.ctx.get("sessionProjections")?.registrations;
315
+ if (!(registrations instanceof Map)) {
316
+ 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`);
317
+ return;
318
+ }
319
+ for (const registration of registrations.values()) {
320
+ const cells = registration?.cells;
321
+ if (!(cells instanceof WeakMap)) {
322
+ 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`);
323
+ continue;
324
+ }
325
+ cells.delete(session);
326
+ }
327
+ }
328
+ resetTokenMeterFold(session) {
329
+ const meter = this.ctx.get("tokenMeter");
330
+ if (meter === void 0) {
331
+ 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"`);
332
+ return;
333
+ }
334
+ const states = meter.states;
335
+ if (!(states instanceof WeakMap)) {
336
+ 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"`);
337
+ return;
338
+ }
339
+ states.delete(session);
340
+ }
234
341
  provider = new SessionBranchRdbProvider(this.ctx.sessionPersistence, {
235
342
  getSession: (id) => this.ctx.sessions.get(id),
236
343
  getAgent: (id) => {
237
344
  return this.ctx.get("agents")?.get(id);
238
345
  },
239
346
  flush: (session) => this.ctx.sessions.flush(session),
240
- resetProjections: (session) => {
241
- const registry = this.ctx.get("sessionProjections");
242
- if (registry?.registrations === void 0) return;
243
- for (const registration of registry.registrations.values()) registration.cells.delete(session);
347
+ warn: (message) => {
348
+ this.ctx.logger.warn(message);
244
349
  },
350
+ resetProjections: (session) => this.resetProjectionCells(session),
351
+ resetTokenMeter: (session) => this.resetTokenMeterFold(session),
245
352
  refreshProjectionCache: async (session) => {
246
353
  const cache = this.ctx.get("sessionProjectionCache");
247
354
  if (cache === void 0) return;
248
355
  try {
356
+ if (session.headSeq !== void 0 && cache.truncateTo !== void 0) {
357
+ await cache.truncateTo(session.header, session.inheritedEventCount, session.headSeq);
358
+ return;
359
+ }
249
360
  await cache.write(session);
250
361
  } catch (error) {
251
362
  this.ctx.logger.warn(`session-rdb: projection cache refresh after rewind for "${session.id}" failed (cache stays stale): ${String(error)}`);
@@ -276,4 +387,4 @@ var SessionBranchRdb = class extends SessionBranch {
276
387
  }
277
388
  };
278
389
  //#endregion
279
- export { truncateLiveSession as a, replaceLiveSessionLog as i, SessionBranchRdbProvider as n, locateTurnEnd as r, SessionBranchRdb as t };
390
+ export { truncateLiveSession as a, isLegacyVersion as c, replaceLiveSessionLog as i, SessionBranchRdbProvider as n, adoptLegacyRows as o, locateTurnEnd as r, convertLegacyRows as s, SessionBranchRdb as t };
@@ -0,0 +1,7 @@
1
+ import { u as SessionPersistenceRdb } from "./backend-DpdtxYpz.mjs";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/deletion.d.ts
4
+ declare const SESSION_DELETE_PATH = "/api/session.delete";
5
+ declare function registerSessionDeletion(ctx: Context, persistence: SessionPersistenceRdb): void;
6
+ //#endregion
7
+ export { SESSION_DELETE_PATH, registerSessionDeletion };
@@ -0,0 +1,2 @@
1
+ import { i as registerSessionDeletion, r as SESSION_DELETE_PATH } from "./src-CWTWV7vx.mjs";
2
+ export { SESSION_DELETE_PATH, registerSessionDeletion };