@morlay/session-rdb 0.0.21-alpha.4 → 0.0.21-alpha.6

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 (48) hide show
  1. package/README.md +53 -27
  2. package/dist/artifact.d.mts +27 -2
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{backend-a0xDrUFG.d.mts → backend-E0bmTXpz.d.mts} +233 -27
  5. package/dist/{branch-Dlmbkfjj.mjs → branch-BjUR6wfe.mjs} +31 -15
  6. package/dist/deletion.d.mts +1 -1
  7. package/dist/deletion.mjs +1 -1
  8. package/dist/import.d.mts +1 -1
  9. package/dist/import.mjs +2 -3
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.mjs +4 -4
  12. package/dist/{log-C-rbdFwK.mjs → log-qCNkDjh5.mjs} +93 -22
  13. package/dist/{sqlite-C3LHLzYW.mjs → sqlite-zcXVyMxr.mjs} +85 -10
  14. package/dist/{src-Bn0038Sp.mjs → src-B9X8BKyh.mjs} +210 -38
  15. package/dist/storage.d.mts +2 -1
  16. package/dist/storage.mjs +1 -1
  17. package/dist/testing.d.mts +2 -11
  18. package/dist/testing.mjs +2 -19
  19. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +1 -0
  20. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +2 -0
  21. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +10 -0
  22. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +7 -0
  23. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +1041 -0
  24. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +4 -0
  25. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +13 -0
  26. package/package.json +21 -21
  27. package/src/backend.ts +46 -1
  28. package/src/branch.ts +28 -11
  29. package/src/drizzle/postgres-v3.ts +1 -0
  30. package/src/drizzle/sqlite-v3.ts +1 -0
  31. package/src/entities/v3/event-usage.ts +5 -5
  32. package/src/entities/v3/sessions.ts +5 -0
  33. package/src/import.ts +3 -3
  34. package/src/index.ts +56 -22
  35. package/src/legacy.ts +14 -3
  36. package/src/log.ts +107 -22
  37. package/src/postgres.ts +65 -5
  38. package/src/rows.ts +125 -0
  39. package/src/schema.ts +3 -2
  40. package/src/session-query.ts +27 -1
  41. package/src/sqlite.ts +71 -3
  42. package/src/storage-takeover/projection-cache.ts +40 -15
  43. package/src/storage-takeover/repository.ts +34 -0
  44. package/src/storage-takeover/storage-backend.ts +3 -1
  45. package/src/testing/coordinator-contract.ts +2 -1
  46. package/src/testing.ts +0 -1
  47. package/src/write-guard.ts +5 -0
  48. package/src/testing/helpers.ts +0 -20
@@ -1,6 +1,6 @@
1
- import { A as WriteGuard, O as postgresTableDefs, a as addActivityCount, c as emptyTotals, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, h as eventDimensions, i as COUNTED_EVENT_TYPES, k as toPostgresSchema, l as localDayKey, o as addTokenTotals, r as createStorageRepository, s as addTotals, t as SqliteBackend, u as registerSessionUsage } from "./sqlite-C3LHLzYW.mjs";
2
- import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-C-rbdFwK.mjs";
3
- import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-Dlmbkfjj.mjs";
1
+ import { A as WriteGuard, O as postgresTableDefs, a as addActivityCount, c as emptyTotals, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, h as eventDimensions, i as COUNTED_EVENT_TYPES, k as toPostgresSchema, l as localDayKey, o as addTokenTotals, r as createStorageRepository, s as addTotals, t as SqliteBackend, u as registerSessionUsage } from "./sqlite-zcXVyMxr.mjs";
2
+ import { _ as sessionInsertRow, b as toJsonlArtifact, g as sessionConflictRow, h as scanRows, m as rowToMeta, r as hasLegacyShape, u as repairReadView, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
3
+ import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-BjUR6wfe.mjs";
4
4
  import { registerSessionImport } from "./import.mjs";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { randomUUID } from "node:crypto";
@@ -173,7 +173,7 @@ var PostgresBackend = class PostgresBackend {
173
173
  insertEvents: (events) => this.insertEvents(tx, events),
174
174
  insertBridges: (rows) => this.insertBridges(tx, rows),
175
175
  updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
176
- bumpRevision: (id) => this.bumpRevision(tx, id),
176
+ bumpRevision: (id, lastEventAt) => this.bumpRevision(tx, id, lastEventAt),
177
177
  refreshTitle: (id) => this.refreshTitle(tx, id),
178
178
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
179
179
  getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
@@ -226,8 +226,12 @@ var PostgresBackend = class PostgresBackend {
226
226
  fHeadSequence: headSequence
227
227
  }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
228
228
  }
229
- async bumpRevision(exec, id) {
230
- await exec.update(this.tables["t_sessions"]).set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
229
+ async bumpRevision(exec, id, lastEventAt) {
230
+ const sessions = this.tables["t_sessions"];
231
+ await exec.update(sessions).set({
232
+ fRevision: sql`${sessions.fRevision} + 1`,
233
+ ...lastEventAt === void 0 ? {} : { fLastEventAt: sql`GREATEST(COALESCE(${sessions.fLastEventAt}, 0), ${lastEventAt})` }
234
+ }).where(eq(sessions.fSessionId, id)).execute();
231
235
  }
232
236
  async refreshTitle(exec, id) {
233
237
  const bridges = this.tables["t_session_events"];
@@ -313,6 +317,55 @@ var PostgresBackend = class PostgresBackend {
313
317
  `);
314
318
  }
315
319
  /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
320
+ async listSessionRows(query = {}) {
321
+ const needle = query.query?.trim().toLowerCase() ?? "";
322
+ const like = `%${needle}%`;
323
+ const includeSubagents = query.includeSubagents === true;
324
+ const limit = query.limit ?? 20;
325
+ const offset = query.offset ?? 0;
326
+ const tSessions = this.tables["t_sessions"];
327
+ const tWorkspaces = this.tables["t_workspaces"];
328
+ const tWorkspaceSessions = this.tables["t_workspace_sessions"];
329
+ const workspaceTitle = sql`(SELECT w.f_title FROM ${tWorkspaceSessions} b
330
+ JOIN ${tWorkspaces} w ON w.f_workspace_id = b.f_workspace_id
331
+ WHERE b.f_session_id = s.f_session_id
332
+ ORDER BY b.f_position LIMIT 1)`;
333
+ const where = sql`WHERE (${needle} = ''
334
+ OR lower(COALESCE(s.f_title, '')) LIKE ${like}
335
+ OR lower(COALESCE(${workspaceTitle}, '')) LIKE ${like})
336
+ AND (${includeSubagents} OR s.f_origin IS DISTINCT FROM 'subagent')`;
337
+ const rows = await this.db.execute(sql`
338
+ SELECT s.f_session_id AS session_id,
339
+ s.f_title AS title,
340
+ s.f_origin AS origin,
341
+ s.f_cwd AS cwd,
342
+ s.f_created_at AS created_at,
343
+ (s.f_archived_at IS NOT NULL) AS archived,
344
+ COALESCE(s.f_last_event_at, s.f_created_at) AS updated_at,
345
+ ${workspaceTitle} AS workspace
346
+ FROM ${tSessions} s
347
+ ${where}
348
+ ORDER BY updated_at DESC, s.f_session_id
349
+ LIMIT ${limit} OFFSET ${offset}
350
+ `);
351
+ const counted = await this.db.execute(sql`
352
+ SELECT count(*) AS n FROM ${tSessions} s ${where}
353
+ `);
354
+ return {
355
+ items: rows.rows.map((row) => ({
356
+ sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
357
+ title: typeof row["title"] === "string" ? row["title"] : null,
358
+ origin: typeof row["origin"] === "string" ? row["origin"] : null,
359
+ cwd: typeof row["cwd"] === "string" ? row["cwd"] : null,
360
+ createdAt: numeric(row["created_at"]),
361
+ updatedAt: numeric(row["updated_at"]),
362
+ archived: row["archived"] === true,
363
+ subagent: row["origin"] === "subagent",
364
+ workspace: typeof row["workspace"] === "string" ? row["workspace"] : null
365
+ })),
366
+ total: numeric(counted.rows[0]?.["n"] ?? 0)
367
+ };
368
+ }
316
369
  async usageReport(sinceMs) {
317
370
  const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
318
371
  const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
@@ -404,7 +457,7 @@ var PostgresBackend = class PostgresBackend {
404
457
  ...tokenTotalsOf(row)
405
458
  })),
406
459
  sessions: tokenSessions.rows.map((row) => ({
407
- sessionId: String(row["session_id"]),
460
+ sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
408
461
  title: text(row["title"]),
409
462
  subagent: row["subagent"] === true,
410
463
  archived: row["archived"] === true,
@@ -693,11 +746,96 @@ function registerSessionGc(ctx, persistence) {
693
746
  });
694
747
  }
695
748
  //#endregion
749
+ //#region src/rows.ts
750
+ /**
751
+ * 管理面的会话列表:**我们自己的**一条路由(完整语料,含归档),与官方 `session/list` 分开。
752
+ *
753
+ * 官方那条(`session/list` → `ctx.sessionQuery.listSessions`)按部署策略默认排除已归档——那是给上游
754
+ * UI 用的。归档集的管理动作要有完整列表,所以这里单独给一条:标题直接从会话列取,最后活动时间用
755
+ * 该会话最后一个事件的时间。
756
+ */
757
+ const SESSION_ROWS_PATH = "/api/session.rows";
758
+ /** 一页的条数上限:超过就按上限截断(这个接口给页面用,不是导出通道)。 */
759
+ const MAX_PAGE_SIZE = 200;
760
+ const DEFAULT_PAGE_SIZE = 20;
761
+ function registerSessionRows(ctx, backend) {
762
+ ctx.inject(["webServer", "connection"], (webCtx) => {
763
+ const webServer = webCtx.webServer;
764
+ const connection = webCtx.get("connection");
765
+ return webCtx.effect(() => webServer.register({
766
+ kind: "exact",
767
+ path: SESSION_ROWS_PATH,
768
+ handler: async (req, res) => {
769
+ const rejection = connection.requestRejection(req);
770
+ if (rejection !== void 0) {
771
+ res.writeHead(rejection);
772
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
773
+ return;
774
+ }
775
+ if (req.method !== "POST") {
776
+ res.writeHead(405, { "content-type": "application/json" });
777
+ res.end(JSON.stringify({ error: "method not allowed" }));
778
+ return;
779
+ }
780
+ const chunks = [];
781
+ for await (const chunk of req) chunks.push(chunk);
782
+ const raw = Buffer.concat(chunks).toString("utf8");
783
+ let envelope = {};
784
+ if (raw !== "") try {
785
+ envelope = JSON.parse(raw);
786
+ } catch {
787
+ res.writeHead(400, { "content-type": "application/json" });
788
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
789
+ return;
790
+ }
791
+ const page = Number.isSafeInteger(envelope.page) && (envelope.page ?? 0) > 0 ? envelope.page : 1;
792
+ const requested = envelope.pageSize;
793
+ const pageSize = Number.isSafeInteger(requested) && (requested ?? 0) > 0 ? Math.min(requested, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
794
+ try {
795
+ const { items, total } = await backend.listSessionRows({
796
+ ...envelope.query === void 0 ? {} : { query: envelope.query },
797
+ ...envelope.includeSubagents === void 0 ? {} : { includeSubagents: envelope.includeSubagents },
798
+ limit: pageSize,
799
+ offset: (page - 1) * pageSize
800
+ });
801
+ res.writeHead(200, { "content-type": "application/json" });
802
+ res.end(JSON.stringify({
803
+ items,
804
+ total,
805
+ page,
806
+ pageSize
807
+ }));
808
+ } catch (error) {
809
+ res.writeHead(500, { "content-type": "application/json" });
810
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session rows failed" }));
811
+ }
812
+ }
813
+ }), "session-rdb: session rows route");
814
+ });
815
+ }
816
+ //#endregion
696
817
  //#region src/session-query.ts
697
818
  var SessionQueryRdb = class extends SessionQueryEngine {
698
819
  constructor(ctx, config = {}) {
699
820
  super(ctx, config);
700
821
  }
822
+ /**
823
+ * 官方列表(`session/list`)**默认不含已归档**:归档是「收起来」的会话,列表与搜索都不该把它混进
824
+ * 日常集合(上游 client 再用 workspace registry 的归档集做「只看归档」的派生)。
825
+ *
826
+ * 完整语料(含归档)走我们自己的管理面路由 `SESSION_ROWS_PATH`——两条路分开,不在一处混。
827
+ */
828
+ async listSessions(signal) {
829
+ const records = await super.listSessions(signal);
830
+ const archived = this.archivedSessionIds();
831
+ if (archived.size === 0) return records;
832
+ return records.filter((record) => !archived.has(String(record.header.id)));
833
+ }
834
+ /** 归档集合来自 workspace registry;服务未就绪时保守返回空集(宁可多给,不可漏掉可见会话)。 */
835
+ archivedSessionIds() {
836
+ const registry = this.ctx.get("workspaceRegistry");
837
+ return new Set(registry?.archivedSessionIds ?? []);
838
+ }
701
839
  async searchSessions() {
702
840
  throw searchDisabled();
703
841
  }
@@ -743,11 +881,19 @@ var SessionProjectionCacheRdb = class extends Service {
743
881
  if (record === void 0) return void 0;
744
882
  return identityMatches(record.identity, expected) ? record : void 0;
745
883
  }
746
- cachedSnapshot(meta, inheritedEventCount, keys) {
747
- const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
884
+ /**
885
+ * header-only 读(会话列表这类):上游 0.1.7 起只给 header 与 keys——它要求的是**生命周期**身份,
886
+ * 不再要求调用方提供 inherited cut(那个 cut 只有写路径与 hydration 用得上)。
887
+ */
888
+ cachedSnapshot(meta, keys) {
889
+ const record = this.recordForLifecycle(meta);
748
890
  const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
749
891
  return this.withDirectTitle(meta.id, keys, snapshot);
750
892
  }
893
+ recordForLifecycle(meta) {
894
+ const record = this.lookup(meta.id);
895
+ return record === void 0 || !identityMatches(record.identity, lifecycleIdentityOf(meta)) ? void 0 : record;
896
+ }
751
897
  withDirectTitle(id, keys, snapshot) {
752
898
  if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
753
899
  if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
@@ -762,15 +908,10 @@ var SessionProjectionCacheRdb = class extends Service {
762
908
  values
763
909
  };
764
910
  }
765
- cachedPredecessorTitle(meta, inheritedEventCount) {
766
- const expected = identityOf(meta, inheritedEventCount);
911
+ cachedPredecessorTitle(meta) {
767
912
  const record = this.lookup(meta.id);
768
- if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return;
769
- const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
770
- return title === void 0 ? void 0 : {
771
- ...title,
772
- asOfSeq: -1
773
- };
913
+ if (record === void 0 || !predecessorIdentityMatches(record.identity, lifecycleIdentityOf(meta))) return;
914
+ return this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
774
915
  }
775
916
  viewRecord(record, keys) {
776
917
  const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
@@ -886,6 +1027,15 @@ function detachJson(rows) {
886
1027
  if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
887
1028
  return JSON.parse(text);
888
1029
  }
1030
+ /** header 能证明的生命周期身份(不含 inherited cut):header-only 读只比对它。 */
1031
+ function lifecycleIdentityOf(header) {
1032
+ return {
1033
+ formatVersion: header.version,
1034
+ createdAt: header.createdAt,
1035
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
1036
+ isSeeded: header.isSeeded
1037
+ };
1038
+ }
889
1039
  function identityOf(header, inheritedEventCount) {
890
1040
  const cut = SessionLogOffset(inheritedEventCount);
891
1041
  if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
@@ -904,7 +1054,7 @@ function predecessorIdentityMatches(stored, expected) {
904
1054
  return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
905
1055
  }
906
1056
  function lifecycleIdentityMatches(stored, expected) {
907
- return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
1057
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded;
908
1058
  }
909
1059
  //#endregion
910
1060
  //#region src/storage-takeover/storage-backend.ts
@@ -1018,11 +1168,12 @@ function workspaceRecordOf(value) {
1018
1168
  }
1019
1169
  function workspaceStateOf(value) {
1020
1170
  const state = value;
1021
- if (typeof state !== "object" || state === null || typeof state.initialized !== "boolean" || !Array.isArray(state.workspaceIds) || !Array.isArray(state.archivedSessionIds)) throw new TypeError("workspace registry state does not match the stored shape");
1171
+ if (typeof state !== "object" || state === null || typeof state.initialized !== "boolean" || !Array.isArray(state.workspaceIds) || !Array.isArray(state.archivedSessionIds) || !Array.isArray(state.pinnedSessionIds)) throw new TypeError("workspace registry state does not match the stored shape");
1022
1172
  return {
1023
1173
  initialized: state.initialized,
1024
1174
  workspaceIds: state.workspaceIds.map((id) => id),
1025
1175
  archivedSessionIds: state.archivedSessionIds.map((id) => id),
1176
+ pinnedSessionIds: state.pinnedSessionIds.map((id) => id),
1026
1177
  ...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
1027
1178
  };
1028
1179
  }
@@ -1048,6 +1199,16 @@ function installStorageTakeover(ctx, options) {
1048
1199
  }
1049
1200
  //#endregion
1050
1201
  //#region src/index.ts
1202
+ /** 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。 */
1203
+ function maxEventTime(events) {
1204
+ let max;
1205
+ for (const event of events) {
1206
+ const time = event.time;
1207
+ if (typeof time !== "number") continue;
1208
+ if (max === void 0 || time > max) max = time;
1209
+ }
1210
+ return max;
1211
+ }
1051
1212
  const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
1052
1213
  const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
1053
1214
  var SessionDeletionError = class extends Error {
@@ -1306,9 +1467,9 @@ var RdbSessionHandle = class RdbSessionHandle {
1306
1467
  }
1307
1468
  static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
1308
1469
  };
1309
- var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
1470
+ var SessionPersistenceRdb = class extends SessionPersistence {
1310
1471
  config;
1311
- static inject = ["sessions", "settings"];
1472
+ static inject = ["sessions"];
1312
1473
  static Config = z.union([z.object({
1313
1474
  type: z.const("sqlite"),
1314
1475
  path: z.string().required(),
@@ -1338,7 +1499,6 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1338
1499
  writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1339
1500
  })
1340
1501
  })]);
1341
- static settingsNs = "session-rdb";
1342
1502
  name = "session-rdb";
1343
1503
  tracker = new RdbBackendTracker(this.name);
1344
1504
  backend;
@@ -1349,19 +1509,9 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1349
1509
  liveBuffers = /* @__PURE__ */ new Map();
1350
1510
  liveReady = /* @__PURE__ */ new Map();
1351
1511
  constructor(ctx, config, injectedBackend) {
1352
- let resolved = config;
1353
- const settings = ctx.reflect.get("settings");
1354
- if (settings !== void 0) {
1355
- const scope = settings.register(SessionPersistenceRdb.settingsNs, SessionPersistenceRdb.Config, { base: config });
1356
- resolved = scope.get();
1357
- scope.watch(() => {
1358
- ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
1359
- });
1360
- }
1361
1512
  super(ctx);
1362
1513
  this.config = config;
1363
- this.config = resolved;
1364
- this.backend = injectedBackend ?? createBackend(resolved);
1514
+ this.backend = injectedBackend ?? createBackend(config);
1365
1515
  this.ready = this.init();
1366
1516
  this.installLiveRouting(ctx);
1367
1517
  new SessionBranchRdb(this.ctx);
@@ -1371,6 +1521,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1371
1521
  registerSessionExport(this.ctx, this);
1372
1522
  registerSessionGc(this.ctx, this);
1373
1523
  registerSessionUsage(this.ctx, this);
1524
+ registerSessionRows(this.ctx, this.backend);
1374
1525
  installStorageTakeover(this.ctx, {
1375
1526
  repository: this.backend.storage,
1376
1527
  ready: this.ready,
@@ -1578,7 +1729,6 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1578
1729
  if (events.length === 0) return false;
1579
1730
  validateStoredEvents(meta, [...events]);
1580
1731
  const reuse = this.reuseEventIds.get(meta.id);
1581
- if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
1582
1732
  let confirmedHead = -1;
1583
1733
  await this.backend.transaction(async (tx) => {
1584
1734
  if (tornTruncateTo !== void 0) {
@@ -1598,9 +1748,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1598
1748
  nextSeq: head.fHeadSequence + 1
1599
1749
  }, reuse);
1600
1750
  await tx.updateHead(meta.id, headEventId, headSequence);
1601
- await tx.bumpRevision(meta.id);
1751
+ await tx.bumpRevision(meta.id, maxEventTime(events));
1602
1752
  confirmedHead = headSequence;
1603
1753
  });
1754
+ if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
1604
1755
  this.writeGuard.confirmHead(meta.id, confirmedHead);
1605
1756
  this.tracker.materialized(meta.id);
1606
1757
  await this.recordEventCounts(meta.id, events);
@@ -1641,6 +1792,19 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1641
1792
  };
1642
1793
  }
1643
1794
  const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
1795
+ if (hasLegacyShape(preserved)) {
1796
+ const adopted = adoptLegacyRows(row, eventRows);
1797
+ return {
1798
+ meta: adopted.meta,
1799
+ inheritedEventCount: adopted.inheritedEventCount,
1800
+ events: adopted.events,
1801
+ incarnation: row.fIncarnation,
1802
+ revision: row.fRevision,
1803
+ storedCount: eventRows.length,
1804
+ migrated: false,
1805
+ ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
1806
+ };
1807
+ }
1644
1808
  return {
1645
1809
  meta,
1646
1810
  inheritedEventCount: row.fSeedLength ?? 0,
@@ -1683,6 +1847,9 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1683
1847
  }
1684
1848
  async rewriteMigratedLog(id, log) {
1685
1849
  await this.backend.transaction(async (tx) => {
1850
+ const head = await tx.getHead(id);
1851
+ if (!this.writeGuard.has(id)) this.writeGuard.confirmHead(id, head.fHeadSequence);
1852
+ this.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
1686
1853
  await tx.deleteBridgeTail(id, 0);
1687
1854
  await tx.upsertSession({
1688
1855
  meta: log.meta,
@@ -1693,7 +1860,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1693
1860
  nextSeq: 0
1694
1861
  });
1695
1862
  await tx.updateHead(id, headEventId, headSequence);
1696
- await tx.bumpRevision(id);
1863
+ await tx.bumpRevision(id, maxEventTime(log.events));
1697
1864
  });
1698
1865
  }
1699
1866
  async listSnapshots(signal) {
@@ -1784,6 +1951,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1784
1951
  registerReuseEventIds(childId, map) {
1785
1952
  this.reuseEventIds.set(childId, new Map(map));
1786
1953
  }
1954
+ /** 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。 */
1955
+ dropReuseEventIds(childId) {
1956
+ this.reuseEventIds.delete(childId);
1957
+ }
1787
1958
  internals() {
1788
1959
  return {
1789
1960
  backend: this.backend,
@@ -1795,7 +1966,8 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1795
1966
  readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
1796
1967
  listSnapshots: (signal) => this.listSnapshots(signal),
1797
1968
  readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
1798
- registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
1969
+ registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
1970
+ dropReuseEventIds: (childId) => this.dropReuseEventIds(childId)
1799
1971
  };
1800
1972
  }
1801
1973
  rowRevision(row) {
@@ -2005,4 +2177,4 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
2005
2177
  };
2006
2178
  }
2007
2179
  //#endregion
2008
- export { registerSessionDeletion as i, SessionPersistenceRdb as n, SESSION_DELETE_PATH as r, SessionDeletionError as t };
2180
+ export { registerSessionDeletion as a, SESSION_DELETE_PATH as i, SessionPersistenceRdb as n, SESSION_ROWS_PATH as r, SessionDeletionError as t };
@@ -1,4 +1,4 @@
1
- import { A as tSessionEvents, C as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, D as tEvents, E as tEventUsage, F as tWorkspaceState, I as tWorkspaces, L as WriteGuard, M as tSessions, N as tStorageUnits, O as tPersistenceState, P as tWorkspaceSessions, R as StorageRepository, S as SCHEMA_VERSION, T as eventKind, _ as DEFAULT_BUSY_TIMEOUT_MS, a as SessionRow, b as EventRole, i as EventRow, j as tSessionProjcacheRows, k as tSchemaMeta, n as BackendTx, o as UsageAggregate, r as EventCountBucket, t as Backend, v as EVENT_ENCODING, w as eventDimensions, x as JournalMode, y as EventKind } from "./backend-a0xDrUFG.mjs";
1
+ import { A as eventKind, B as tWorkspaceState, C as EVENT_ENCODING, D as SCHEMA_VERSION, E as JournalMode, F as tSessionEvents, H as WriteGuard, I as tSessionProjcacheRows, L as tSessions, M as tEvents, N as tPersistenceState, O as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, P as tSchemaMeta, R as tStorageUnits, S as DEFAULT_BUSY_TIMEOUT_MS, T as EventRole, U as StorageRepository, V as tWorkspaces, a as SessionListRowsPage, c as UsageAggregate, i as EventRow, j as tEventUsage, k as eventDimensions, n as BackendTx, o as SessionListRowsQuery, r as EventCountBucket, s as SessionRow, t as Backend, w as EventKind, z as tWorkspaceSessions } from "./backend-E0bmTXpz.mjs";
2
2
  import { SessionId } from "@deepseek-ai/dsh-session";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  //#region src/sqlite.d.ts
@@ -52,6 +52,7 @@ declare class SqliteBackend implements Backend {
52
52
  * 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
53
53
  * `f_created_at`(同一时刻写入,口径一致)。
54
54
  */
55
+ listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
55
56
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
56
57
  /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
57
58
  incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
package/dist/storage.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { A as WriteGuard, C as tSessions, D as tWorkspaces, E as tWorkspaceState, S as tSessionProjcacheRows, T as tWorkspaceSessions, _ as tEventUsage, b as tSchemaMeta, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, g as eventKind, h as eventDimensions, m as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, n as openDatabase, p as SCHEMA_VERSION, t as SqliteBackend, v as tEvents, w as tStorageUnits, x as tSessionEvents, y as tPersistenceState } from "./sqlite-C3LHLzYW.mjs";
1
+ import { A as WriteGuard, C as tSessions, D as tWorkspaces, E as tWorkspaceState, S as tSessionProjcacheRows, T as tWorkspaceSessions, _ as tEventUsage, b as tSchemaMeta, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, g as eventKind, h as eventDimensions, m as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, n as openDatabase, p as SCHEMA_VERSION, t as SqliteBackend, v as tEvents, w as tStorageUnits, x as tSessionEvents, y as tPersistenceState } from "./sqlite-zcXVyMxr.mjs";
2
2
  export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
@@ -1,16 +1,7 @@
1
- import { g as truncateLiveSession } from "./backend-a0xDrUFG.mjs";
1
+ import { v as truncateLiveSession } from "./backend-E0bmTXpz.mjs";
2
2
  import { SessionPersistence } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context, Fiber } from "@deepseek-ai/cordis";
5
- import { SettingsNamespace, SettingsProvider } from "@deepseek-ai/dsh-settings";
6
- //#region src/testing/helpers.d.ts
7
- declare class EmptySettings extends SettingsProvider {
8
- constructor(ctx: Context);
9
- get writable(): boolean;
10
- protected load(): Promise<Record<string, unknown>>;
11
- protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void>;
12
- }
13
- //#endregion
14
5
  //#region src/testing/contract.d.ts
15
6
  interface ContractBackendInstance {
16
7
  persistence: SessionPersistence;
@@ -33,4 +24,4 @@ interface CoordinatorFixture {
33
24
  }
34
25
  declare function runCoordinatorContract(name: string, makeFixture: () => Promise<CoordinatorFixture>): void;
35
26
  //#endregion
36
- export { type ContractBackend, type CoordinatorFixture, EmptySettings, appendLog, meta, oneTurnLog, runCoordinatorContract, runPersistenceContract, truncateLiveSession };
27
+ export { type ContractBackend, type CoordinatorFixture, appendLog, meta, oneTurnLog, runCoordinatorContract, runPersistenceContract, truncateLiveSession };
package/dist/testing.mjs CHANGED
@@ -1,28 +1,11 @@
1
- import { a as truncateLiveSession } from "./branch-Dlmbkfjj.mjs";
1
+ import { a as truncateLiveSession } from "./branch-BjUR6wfe.mjs";
2
2
  import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
3
3
  import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
- import { SettingsProvider } from "@deepseek-ai/dsh-settings";
6
5
  import { MessageId, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
7
6
  //#region \0rolldown/runtime.js
8
7
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
8
  //#endregion
10
- //#region src/testing/helpers.ts
11
- var EmptySettings = class extends SettingsProvider {
12
- constructor(ctx) {
13
- super(ctx);
14
- }
15
- get writable() {
16
- return true;
17
- }
18
- async load() {
19
- return {};
20
- }
21
- persist(_ns, _section) {
22
- return Promise.resolve();
23
- }
24
- };
25
- //#endregion
26
9
  //#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/tinyrainbow.Ht9iggcq.js
27
10
  var b$1 = {
28
11
  reset: [0, 0],
@@ -15623,4 +15606,4 @@ function runCoordinatorContract(name, makeFixture) {
15623
15606
  });
15624
15607
  }
15625
15608
  //#endregion
15626
- export { EmptySettings, appendLog, meta, oneTurnLog, runCoordinatorContract, runPersistenceContract, truncateLiveSession };
15609
+ export { appendLog, meta, oneTurnLog, runCoordinatorContract, runPersistenceContract, truncateLiveSession };
@@ -0,0 +1 @@
1
+ ALTER TABLE "t_sessions" ADD COLUMN "f_agent_preset" text;
@@ -0,0 +1,2 @@
1
+ -- 见 SQLite 侧同名迁移的说明:钉住的会话集合用会话行的序号列表达。
2
+ ALTER TABLE "t_sessions" ADD COLUMN "f_pinned_seq" integer;
@@ -0,0 +1,10 @@
1
+ -- 见 SQLite 侧同名迁移的说明:最后活动时间物化到会话行,写路径维护,这里一次性回填。
2
+ ALTER TABLE "t_sessions" ADD COLUMN "f_last_event_at" bigint;
3
+ --> statement-breakpoint
4
+ UPDATE "t_sessions" s
5
+ SET "f_last_event_at" = (
6
+ SELECT MAX(e.f_created_at)
7
+ FROM "t_session_events" se
8
+ JOIN "t_events" e ON e.f_event_id = se.f_event_id
9
+ WHERE se.f_session_id = s.f_session_id
10
+ );
@@ -0,0 +1,7 @@
1
+ -- 只加一列:`agentPreset` 是会话 header 的创建事实,会话投影 `agentPreset` 的 `init` 只读 header
2
+ -- (缺了就被算成 null 并缓存下来),所以列式 header 必须存它。
3
+ --
4
+ -- 生成器另外还想重建 `t_event_usage`(实体的 tokens 列没有 default,而既有迁移建的表带 DEFAULT 0)。
5
+ -- 那次重建与本次改动无关,且会在几十万行的库上重写整表,所以不在这里应用;实体的 default 差异
6
+ -- 留待单独处理。
7
+ ALTER TABLE `t_sessions` ADD `f_agent_preset` text;