@morlay/ui-conversation-message-actions 0.0.14 → 0.0.15

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.
package/dist/testing.mjs CHANGED
@@ -19191,6 +19191,21 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
19191
19191
  for (const event of events) lines.push(JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event)));
19192
19192
  return lines.join("\n");
19193
19193
  }
19194
+ /**
19195
+ * 事件行 JSON → `session/title` 的标题文本。存储层写路径保证 `f_data` 是完整
19196
+ * 事件 JSON;形状不符(或损坏)按「无标题」处理——标题是派生显示数据。
19197
+ * @param data - `t_events.f_data` 原文。
19198
+ * @returns 标题文本,或 `undefined`。
19199
+ */
19200
+ function titleOfEventData(data) {
19201
+ try {
19202
+ const value = JSON.parse(data);
19203
+ const title = value.data?.title ?? value.title;
19204
+ return typeof title === "string" ? title : void 0;
19205
+ } catch {
19206
+ return;
19207
+ }
19208
+ }
19194
19209
  //#endregion
19195
19210
  //#region ../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.1/node_modules/drizzle-orm/column-builder.js
19196
19211
  var ColumnBuilder = class {
@@ -22867,7 +22882,13 @@ const sessions = {
22867
22882
  f_revision: {
22868
22883
  type: "integer",
22869
22884
  notNull: true
22870
- }
22885
+ },
22886
+ /** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
22887
+ f_archived_at: { type: "bigint" },
22888
+ /** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
22889
+ f_title: { type: "text" },
22890
+ /** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
22891
+ f_title_seq: { type: "integer" }
22871
22892
  }
22872
22893
  };
22873
22894
  //#endregion
@@ -22973,20 +22994,193 @@ const sessionEvents$1 = {
22973
22994
  indexes: { idx_session_events_event_id: { columns: ["f_event_id"] } }
22974
22995
  };
22975
22996
  //#endregion
22997
+ //#region ../session-rdb/src/entities/v3/storage-units.ts
22998
+ /** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
22999
+ const storageUnits = {
23000
+ name: "t_storage_units",
23001
+ columns: {
23002
+ f_name: {
23003
+ type: "text",
23004
+ primaryKey: true
23005
+ },
23006
+ f_version: {
23007
+ type: "integer",
23008
+ notNull: true
23009
+ }
23010
+ }
23011
+ };
23012
+ //#endregion
23013
+ //#region ../session-rdb/src/entities/v3/workspaces.ts
23014
+ /**
23015
+ * workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
23016
+ * 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
23017
+ * 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
23018
+ */
23019
+ const workspaces = {
23020
+ name: "t_workspaces",
23021
+ columns: {
23022
+ f_id: {
23023
+ type: "serial",
23024
+ primaryKey: true
23025
+ },
23026
+ f_workspace_id: {
23027
+ type: "text",
23028
+ notNull: true,
23029
+ unique: true
23030
+ },
23031
+ f_path: {
23032
+ type: "text",
23033
+ notNull: true
23034
+ },
23035
+ f_title: {
23036
+ type: "text",
23037
+ notNull: true
23038
+ },
23039
+ f_created_at: {
23040
+ type: "text",
23041
+ notNull: true
23042
+ },
23043
+ f_updated_at: {
23044
+ type: "text",
23045
+ notNull: true
23046
+ },
23047
+ f_position: {
23048
+ type: "integer",
23049
+ notNull: true,
23050
+ default: -1
23051
+ }
23052
+ }
23053
+ };
23054
+ //#endregion
23055
+ //#region ../session-rdb/src/entities/v3/workspace-sessions.ts
23056
+ /**
23057
+ * workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
23058
+ * 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
23059
+ * session 反查归属 workspace。
23060
+ */
23061
+ const workspaceSessions = {
23062
+ name: "t_workspace_sessions",
23063
+ columns: {
23064
+ f_id: {
23065
+ type: "serial",
23066
+ primaryKey: true
23067
+ },
23068
+ f_workspace_id: {
23069
+ type: "text",
23070
+ notNull: true,
23071
+ references: {
23072
+ table: "t_workspaces",
23073
+ column: "f_workspace_id",
23074
+ onDelete: "cascade"
23075
+ }
23076
+ },
23077
+ f_session_id: {
23078
+ type: "text",
23079
+ notNull: true
23080
+ },
23081
+ f_position: {
23082
+ type: "integer",
23083
+ notNull: true
23084
+ }
23085
+ },
23086
+ uniques: { uq_workspace_sessions_workspace_session: { columns: ["f_workspace_id", "f_session_id"] } },
23087
+ indexes: { idx_workspace_sessions_session_id: { columns: ["f_session_id"] } }
23088
+ };
23089
+ //#endregion
23090
+ //#region ../session-rdb/src/entities/v3/workspace-state.ts
23091
+ /**
23092
+ * workspace 域 global 单例(上游 `workspaceDomainState`):`initialized` 是
23093
+ * 引导标记,`pendingMutation` 拆成操作与目标两列(create / delete 的可恢复
23094
+ * 两写标记)。显示顺序(`workspaceIds`)在 `t_workspaces.f_position`,
23095
+ * 归档集在 `t_session_archives`。
23096
+ */
23097
+ const workspaceState = {
23098
+ name: "t_workspace_state",
23099
+ columns: {
23100
+ f_singleton: {
23101
+ type: "integer",
23102
+ primaryKey: true
23103
+ },
23104
+ f_initialized: {
23105
+ type: "integer",
23106
+ notNull: true
23107
+ },
23108
+ f_pending_operation: { type: "text" },
23109
+ f_pending_workspace_id: { type: "text" }
23110
+ },
23111
+ checks: { ck_workspace_state_singleton: { expression: "f_singleton = 1" } }
23112
+ };
23113
+ //#endregion
23114
+ //#region ../session-rdb/src/entities/v3/session-projcache-rows.ts
23115
+ /**
23116
+ * 投影 checkpoint 行(上游 checkpoint 的 `(sessionId, key, ver, seq, val)`
23117
+ * 逐行落库):`f_seq` 是该行的日志水位、`f_ver` 是投影单元的 stateVersion。
23118
+ * 行是折出捷径、非权威,版本不匹配时整行丢弃。
23119
+ *
23120
+ * checkpoint 的日志 identity 不另存一份:它与 `t_sessions` 的行是 1:1,直接
23121
+ * 复用该行的 `f_version` / `f_created_at` / `f_cwd` / `f_seed_length` 做校验。
23122
+ */
23123
+ const sessionProjcacheRows = {
23124
+ name: "t_session_projcache_row",
23125
+ columns: {
23126
+ f_id: {
23127
+ type: "serial",
23128
+ primaryKey: true
23129
+ },
23130
+ f_session_id: {
23131
+ type: "text",
23132
+ notNull: true,
23133
+ references: {
23134
+ table: "t_sessions",
23135
+ column: "f_session_id",
23136
+ onDelete: "cascade"
23137
+ }
23138
+ },
23139
+ f_key: {
23140
+ type: "text",
23141
+ notNull: true
23142
+ },
23143
+ f_ver: {
23144
+ type: "integer",
23145
+ notNull: true
23146
+ },
23147
+ f_seq: {
23148
+ type: "integer",
23149
+ notNull: true
23150
+ },
23151
+ f_val: {
23152
+ type: "text",
23153
+ notNull: true
23154
+ }
23155
+ },
23156
+ uniques: { uq_session_projcache_row_session_key: { columns: ["f_session_id", "f_key"] } },
23157
+ indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
23158
+ };
23159
+ //#endregion
22976
23160
  //#region ../session-rdb/src/entities/v3/index.ts
22977
23161
  const sqliteTableDefs = [
22978
23162
  persistenceState,
22979
23163
  schemaMeta,
22980
23164
  sessions,
22981
23165
  events,
22982
- sessionEvents$1
23166
+ sessionEvents$1,
23167
+ storageUnits,
23168
+ workspaces,
23169
+ workspaceSessions,
23170
+ workspaceState,
23171
+ sessionProjcacheRows
22983
23172
  ];
22984
23173
  const postgresTableDefs = [
22985
23174
  persistenceState,
22986
23175
  schemaMeta,
22987
23176
  sessions,
22988
23177
  events,
22989
- sessionEvents$1
23178
+ sessionEvents$1,
23179
+ storageUnits,
23180
+ workspaces,
23181
+ workspaceSessions,
23182
+ workspaceState,
23183
+ sessionProjcacheRows
22990
23184
  ];
22991
23185
  ({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
22992
23186
  //#endregion
@@ -22999,6 +23193,11 @@ sqliteTables["t_schema_meta"];
22999
23193
  const tSessions = sqliteTables["t_sessions"];
23000
23194
  const tEvents = sqliteTables["t_events"];
23001
23195
  const tSessionEvents = sqliteTables["t_session_events"];
23196
+ const tStorageUnits = sqliteTables["t_storage_units"];
23197
+ const tWorkspaces = sqliteTables["t_workspaces"];
23198
+ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
23199
+ const tWorkspaceState = sqliteTables["t_workspace_state"];
23200
+ const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
23002
23201
  const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
23003
23202
  function eventKind(event) {
23004
23203
  switch (event.type) {
@@ -23013,6 +23212,8 @@ function eventKind(event) {
23013
23212
  case "session/end-seed": return "turn";
23014
23213
  case "tool/call":
23015
23214
  case "tool/result":
23215
+ case "tool/ptc-dispatch-start":
23216
+ case "tool/ptc-dispatch":
23016
23217
  case "tool/code-dispatch-start":
23017
23218
  case "tool/code-dispatch": return "tool";
23018
23219
  case "request/header":
@@ -23086,6 +23287,8 @@ function eventDimensions(event) {
23086
23287
  name: typeof data["name"] === "string" ? data["name"] : "",
23087
23288
  actionId: typeof data["callId"] === "string" ? data["callId"] : ""
23088
23289
  };
23290
+ case "tool/ptc-dispatch-start":
23291
+ case "tool/ptc-dispatch":
23089
23292
  case "tool/code-dispatch-start":
23090
23293
  case "tool/code-dispatch": return {
23091
23294
  kind,
@@ -23288,6 +23491,254 @@ function migrate$1(db, config) {
23288
23491
  return migrateSync(readMigrationFiles(config), db.session, config);
23289
23492
  }
23290
23493
  //#endregion
23494
+ //#region ../session-rdb/src/storage-takeover/repository.ts
23495
+ /**
23496
+ * storages 接管表的方言无关实现:SQLite 与 PostgreSQL 的 drizzle 查询构建器
23497
+ * 共享同一套调用形状(SQLite 走同步 `.all()`/`.get()`/`.run()`,PostgreSQL
23498
+ * 走 thenable),差别由本模块的执行 helper 吸收。
23499
+ *
23500
+ * workspace 域是权威数据:记录、归属、显示顺序与归档集都拆成语义表,每次写入
23501
+ * 在介质事务内完成(`host.writeAtomically`),不留部分应用的中间态。
23502
+ */
23503
+ /** 多行查询:SQLite 同步 `.all()`,PostgreSQL await。 */
23504
+ async function allRows(query) {
23505
+ const q = query;
23506
+ if (typeof q.all === "function") return q.all();
23507
+ return await query;
23508
+ }
23509
+ /** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
23510
+ async function oneRow(query) {
23511
+ const q = query;
23512
+ if (typeof q.get === "function") return q.get();
23513
+ return (await query)[0];
23514
+ }
23515
+ /** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
23516
+ async function runQuery(query) {
23517
+ const q = query;
23518
+ if (typeof q.run === "function") {
23519
+ q.run();
23520
+ return;
23521
+ }
23522
+ await query;
23523
+ }
23524
+ /** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
23525
+ function identityOfSession(row) {
23526
+ return {
23527
+ formatVersion: row.fVersion,
23528
+ createdAt: row.fCreatedAt,
23529
+ ...row.fCwd === null ? {} : { cwd: row.fCwd },
23530
+ isSeeded: row.fSeedLength !== null,
23531
+ ...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
23532
+ };
23533
+ }
23534
+ /** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
23535
+ function seqCursor(seq) {
23536
+ return seq === -1 ? -1 : SessionSeq$1(seq);
23537
+ }
23538
+ /** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
23539
+ function pendingMutationOf(row) {
23540
+ const operation = row.fPendingOperation;
23541
+ if (operation !== "create" && operation !== "delete") return void 0;
23542
+ return {
23543
+ operation,
23544
+ workspaceId: row.fPendingWorkspaceId ?? ""
23545
+ };
23546
+ }
23547
+ /**
23548
+ * 构建一个介质的 storages 接管访问层。
23549
+ * @param host - 介质句柄、方言表对象与事务执行器。
23550
+ * @returns 方言无关的访问层。
23551
+ */
23552
+ function createStorageRepository(host) {
23553
+ const tables = host.tables;
23554
+ const tUnits = tables["t_storage_units"];
23555
+ const tSessions = tables["t_sessions"];
23556
+ const tWorkspaces = tables["t_workspaces"];
23557
+ const tWorkspaceSessions = tables["t_workspace_sessions"];
23558
+ const tWorkspaceState = tables["t_workspace_state"];
23559
+ const tProjcacheRows = tables["t_session_projcache_row"];
23560
+ /** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
23561
+ const sessionIdentity = {
23562
+ fSessionId: tSessions.fSessionId,
23563
+ fVersion: tSessions.fVersion,
23564
+ fCreatedAt: tSessions.fCreatedAt,
23565
+ fCwd: tSessions.fCwd,
23566
+ fSeedLength: tSessions.fSeedLength
23567
+ };
23568
+ const dbx = async () => await host.db();
23569
+ return {
23570
+ async readUnitVersion(name) {
23571
+ return (await oneRow((await dbx()).select().from(tUnits).where(eq(tUnits.fName, name))))?.fVersion;
23572
+ },
23573
+ async insertUnitVersion(name, version) {
23574
+ await runQuery((await dbx()).insert(tUnits).values({
23575
+ fName: name,
23576
+ fVersion: version
23577
+ }).onConflictDoNothing());
23578
+ },
23579
+ async listWorkspaces() {
23580
+ const db = await dbx();
23581
+ const rows = await allRows(db.select().from(tWorkspaces));
23582
+ const links = await allRows(db.select().from(tWorkspaceSessions));
23583
+ const sessions = /* @__PURE__ */ new Map();
23584
+ for (const link of links) {
23585
+ const owned = sessions.get(link.fWorkspaceId) ?? [];
23586
+ owned.push({
23587
+ id: link.fSessionId,
23588
+ position: link.fPosition
23589
+ });
23590
+ sessions.set(link.fWorkspaceId, owned);
23591
+ }
23592
+ return rows.map((row) => ({
23593
+ id: row.fWorkspaceId,
23594
+ record: {
23595
+ path: row.fPath,
23596
+ title: row.fTitle,
23597
+ sessionIds: (sessions.get(row.fWorkspaceId) ?? []).sort((left, right) => left.position - right.position).map((entry) => SessionId(entry.id)),
23598
+ createdAt: row.fCreatedAt,
23599
+ updatedAt: row.fUpdatedAt
23600
+ }
23601
+ }));
23602
+ },
23603
+ async putWorkspace(id, record) {
23604
+ await host.writeAtomically(async () => {
23605
+ const db = await dbx();
23606
+ const values = {
23607
+ fWorkspaceId: id,
23608
+ fPath: record.path,
23609
+ fTitle: record.title,
23610
+ fCreatedAt: record.createdAt,
23611
+ fUpdatedAt: record.updatedAt
23612
+ };
23613
+ await runQuery(db.insert(tWorkspaces).values(values).onConflictDoUpdate({
23614
+ target: tWorkspaces.fWorkspaceId,
23615
+ set: values
23616
+ }));
23617
+ await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
23618
+ const links = record.sessionIds.map((sessionId, position) => ({
23619
+ fWorkspaceId: id,
23620
+ fSessionId: sessionId,
23621
+ fPosition: position
23622
+ }));
23623
+ if (links.length > 0) await runQuery(db.insert(tWorkspaceSessions).values(links));
23624
+ });
23625
+ },
23626
+ async deleteWorkspace(id) {
23627
+ await host.writeAtomically(async () => {
23628
+ const db = await dbx();
23629
+ await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
23630
+ await runQuery(db.delete(tWorkspaces).where(eq(tWorkspaces.fWorkspaceId, id)));
23631
+ });
23632
+ },
23633
+ async readWorkspaceState() {
23634
+ const db = await dbx();
23635
+ const row = await oneRow(db.select().from(tWorkspaceState).where(eq(tWorkspaceState.fSingleton, 1)));
23636
+ if (row === void 0) return null;
23637
+ const ordered = await allRows(db.select().from(tWorkspaces).where(gte(tWorkspaces.fPosition, 0)).orderBy(tWorkspaces.fPosition));
23638
+ const archives = await allRows(db.select({ fSessionId: tSessions.fSessionId }).from(tSessions).where(isNotNull(tSessions.fArchivedAt)).orderBy(tSessions.fArchivedAt));
23639
+ const pendingMutation = pendingMutationOf(row);
23640
+ return {
23641
+ initialized: row.fInitialized !== 0,
23642
+ workspaceIds: ordered.map((entry) => entry.fWorkspaceId),
23643
+ archivedSessionIds: archives.map((entry) => entry.fSessionId),
23644
+ ...pendingMutation === void 0 ? {} : { pendingMutation }
23645
+ };
23646
+ },
23647
+ async writeWorkspaceState(state) {
23648
+ await host.writeAtomically(async () => {
23649
+ const db = await dbx();
23650
+ const pending = state.pendingMutation;
23651
+ const values = {
23652
+ fSingleton: 1,
23653
+ fInitialized: state.initialized ? 1 : 0,
23654
+ fPendingOperation: pending === void 0 || typeof pending.operation !== "string" ? null : pending.operation,
23655
+ fPendingWorkspaceId: pending === void 0 || typeof pending.workspaceId !== "string" ? null : pending.workspaceId
23656
+ };
23657
+ await runQuery(db.insert(tWorkspaceState).values(values).onConflictDoUpdate({
23658
+ target: tWorkspaceState.fSingleton,
23659
+ set: values
23660
+ }));
23661
+ await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
23662
+ for (const [position, workspaceId] of state.workspaceIds.entries()) await runQuery(db.update(tWorkspaces).set({ fPosition: position }).where(eq(tWorkspaces.fWorkspaceId, workspaceId)));
23663
+ const archived = state.archivedSessionIds;
23664
+ await runQuery(archived.length === 0 ? db.update(tSessions).set({ fArchivedAt: null }).where(isNotNull(tSessions.fArchivedAt)) : db.update(tSessions).set({ fArchivedAt: null }).where(and(isNotNull(tSessions.fArchivedAt), notInArray(tSessions.fSessionId, archived))));
23665
+ const stamp = Date.now();
23666
+ for (const sessionId of archived) await runQuery(db.update(tSessions).set({ fArchivedAt: sql`COALESCE(${tSessions.fArchivedAt}, ${stamp})` }).where(eq(tSessions.fSessionId, sessionId)));
23667
+ });
23668
+ },
23669
+ async loadProjcache() {
23670
+ const db = await dbx();
23671
+ const rows = await allRows(db.select().from(tProjcacheRows));
23672
+ if (rows.length === 0) return [];
23673
+ const sessions = await allRows(db.select(sessionIdentity).from(tSessions));
23674
+ const entries = /* @__PURE__ */ new Map();
23675
+ for (const session of sessions) entries.set(session.fSessionId, {
23676
+ sessionId: SessionId(session.fSessionId),
23677
+ identity: identityOfSession(session),
23678
+ rows: {}
23679
+ });
23680
+ for (const row of rows) {
23681
+ const entry = entries.get(row.fSessionId);
23682
+ if (entry === void 0) continue;
23683
+ entry.rows[row.fKey] = {
23684
+ ver: row.fVer,
23685
+ seq: seqCursor(row.fSeq),
23686
+ val: JSON.parse(row.fVal)
23687
+ };
23688
+ }
23689
+ return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
23690
+ },
23691
+ async putProjcache(sessionId, rows) {
23692
+ await host.writeAtomically(async () => {
23693
+ const db = await dbx();
23694
+ await runQuery(db.delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
23695
+ const values = Object.entries(rows).map(([key, row]) => ({
23696
+ fSessionId: sessionId,
23697
+ fKey: key,
23698
+ fVer: row.ver,
23699
+ fSeq: row.seq,
23700
+ fVal: JSON.stringify(row.val)
23701
+ }));
23702
+ if (values.length > 0) await runQuery(db.insert(tProjcacheRows).values(values));
23703
+ });
23704
+ },
23705
+ async deleteProjcache(sessionId) {
23706
+ await runQuery((await dbx()).delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
23707
+ },
23708
+ ...host.dbSync === void 0 ? {} : {
23709
+ readProjcacheSync: (sessionId) => {
23710
+ const db = host.dbSync();
23711
+ const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
23712
+ if (stored.length === 0) return void 0;
23713
+ const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
23714
+ if (session === void 0) return void 0;
23715
+ const rows = {};
23716
+ for (const row of stored) rows[row.fKey] = {
23717
+ ver: row.fVer,
23718
+ seq: seqCursor(row.fSeq),
23719
+ val: JSON.parse(row.fVal)
23720
+ };
23721
+ return {
23722
+ sessionId: SessionId(sessionId),
23723
+ identity: identityOfSession(session),
23724
+ rows
23725
+ };
23726
+ },
23727
+ readSessionTitleSync: (sessionId) => {
23728
+ const row = host.dbSync().select({
23729
+ fTitle: tSessions.fTitle,
23730
+ fTitleSeq: tSessions.fTitleSeq
23731
+ }).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
23732
+ if (row === void 0 || row.fTitle === null || row.fTitleSeq === null) return void 0;
23733
+ return {
23734
+ title: row.fTitle,
23735
+ seq: row.fTitleSeq
23736
+ };
23737
+ }
23738
+ }
23739
+ };
23740
+ }
23741
+ //#endregion
23291
23742
  //#region ../session-rdb/src/sqlite.ts
23292
23743
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
23293
23744
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
@@ -23362,12 +23813,60 @@ var SqliteBackend = class SqliteBackend {
23362
23813
  options;
23363
23814
  kind = "sqlite";
23364
23815
  storeIdentity;
23816
+ /** storages 接管表访问层;句柄在 open() 完成后解析。 */
23817
+ storage;
23365
23818
  dbPath = "";
23366
23819
  db;
23820
+ dbReady;
23821
+ resolveDb;
23822
+ rejectDb;
23367
23823
  constructor(options) {
23368
23824
  this.options = options;
23825
+ this.dbReady = new Promise((resolve, reject) => {
23826
+ this.resolveDb = resolve;
23827
+ this.rejectDb = reject;
23828
+ });
23829
+ this.dbReady.catch(() => {});
23830
+ this.storage = createStorageRepository({
23831
+ db: () => this.dbReady,
23832
+ dbSync: () => {
23833
+ if (this.db === void 0) throw new Error("sqlite session database is not open");
23834
+ return this.db;
23835
+ },
23836
+ writeAtomically: (fn) => enqueueSqliteTx(this.dbPath, async () => {
23837
+ const db = await this.dbReady;
23838
+ db.$client.exec("BEGIN IMMEDIATE");
23839
+ try {
23840
+ const result = await fn();
23841
+ db.$client.exec("COMMIT");
23842
+ return result;
23843
+ } catch (error) {
23844
+ try {
23845
+ db.$client.exec("ROLLBACK");
23846
+ } catch {}
23847
+ throw error;
23848
+ }
23849
+ }),
23850
+ tables: {
23851
+ t_sessions: tSessions,
23852
+ t_storage_units: tStorageUnits,
23853
+ t_workspaces: tWorkspaces,
23854
+ t_workspace_sessions: tWorkspaceSessions,
23855
+ t_workspace_state: tWorkspaceState,
23856
+ t_session_projcache_row: tSessionProjcacheRows
23857
+ }
23858
+ });
23369
23859
  }
23370
23860
  async open() {
23861
+ try {
23862
+ await this.doOpen();
23863
+ this.resolveDb(this.db);
23864
+ } catch (error) {
23865
+ this.rejectDb(error);
23866
+ throw error;
23867
+ }
23868
+ }
23869
+ async doOpen() {
23371
23870
  const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
23372
23871
  this.dbPath = actual;
23373
23872
  if (actual !== ":memory:") {
@@ -23433,6 +23932,7 @@ var SqliteBackend = class SqliteBackend {
23433
23932
  insertBridges: (rows) => this.insertBridges(rows),
23434
23933
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
23435
23934
  bumpRevision: (id) => this.bumpRevision(id),
23935
+ refreshTitle: (id) => this.refreshTitle(id),
23436
23936
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
23437
23937
  getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
23438
23938
  };
@@ -23478,6 +23978,18 @@ var SqliteBackend = class SqliteBackend {
23478
23978
  async bumpRevision(id) {
23479
23979
  this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
23480
23980
  }
23981
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
23982
+ async refreshTitle(id) {
23983
+ const row = this.db.select({
23984
+ fSequence: tSessionEvents.fSequence,
23985
+ fData: tEvents.fData
23986
+ }).from(tSessionEvents).innerJoin(tEvents, eq(tEvents.fEventId, tSessionEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), eq(tEvents.fType, "session/title"))).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
23987
+ const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
23988
+ this.db.update(tSessions).set({
23989
+ fTitle: title ?? null,
23990
+ fTitleSeq: title === void 0 ? null : row.fSequence
23991
+ }).where(eq(tSessions.fSessionId, id)).run();
23992
+ }
23481
23993
  async deleteBridgeTail(id, fromSequence) {
23482
23994
  this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
23483
23995
  }
@@ -23517,12 +24029,46 @@ var PostgresBackend = class PostgresBackend {
23517
24029
  kind = "postgres";
23518
24030
  storeIdentity;
23519
24031
  tables;
24032
+ /** storages 接管表访问层;句柄在 open()(含迁移)完成后解析。 */
24033
+ storage;
24034
+ opened;
24035
+ resolveOpened;
24036
+ rejectOpened;
24037
+ /** 事务期间的连接覆盖:storage 写方法经它加入当前事务。 */
24038
+ txOverride;
23520
24039
  constructor(db, options) {
23521
24040
  this.db = db;
23522
24041
  this.options = options;
23523
24042
  this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
24043
+ this.opened = new Promise((resolve, reject) => {
24044
+ this.resolveOpened = resolve;
24045
+ this.rejectOpened = reject;
24046
+ });
24047
+ this.opened.catch(() => {});
24048
+ this.storage = createStorageRepository({
24049
+ db: () => this.opened.then(() => this.txOverride ?? this.db),
24050
+ writeAtomically: (fn) => this.db.transaction(async (tx) => {
24051
+ const previous = this.txOverride;
24052
+ this.txOverride = tx;
24053
+ try {
24054
+ return await fn();
24055
+ } finally {
24056
+ this.txOverride = previous;
24057
+ }
24058
+ }),
24059
+ tables: this.tables
24060
+ });
23524
24061
  }
23525
24062
  async open() {
24063
+ try {
24064
+ await this.doOpen();
24065
+ this.resolveOpened();
24066
+ } catch (error) {
24067
+ this.rejectOpened(error);
24068
+ throw error;
24069
+ }
24070
+ }
24071
+ async doOpen() {
23526
24072
  const schema = this.options.schema ?? "public";
23527
24073
  const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
23528
24074
  if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
@@ -23583,6 +24129,7 @@ var PostgresBackend = class PostgresBackend {
23583
24129
  insertBridges: (rows) => this.insertBridges(tx, rows),
23584
24130
  updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
23585
24131
  bumpRevision: (id) => this.bumpRevision(tx, id),
24132
+ refreshTitle: (id) => this.refreshTitle(tx, id),
23586
24133
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
23587
24134
  getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
23588
24135
  };
@@ -23632,6 +24179,20 @@ var PostgresBackend = class PostgresBackend {
23632
24179
  async bumpRevision(exec, id) {
23633
24180
  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();
23634
24181
  }
24182
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
24183
+ async refreshTitle(exec, id) {
24184
+ const bridges = this.tables["t_session_events"];
24185
+ const entities = this.tables["t_events"];
24186
+ const row = (await exec.select({
24187
+ fSequence: bridges.fSequence,
24188
+ fData: entities.fData
24189
+ }).from(bridges).innerJoin(entities, eq(entities.fEventId, bridges.fEventId)).where(and(eq(bridges.fSessionId, id), eq(entities.fType, "session/title"))).orderBy(desc(bridges.fSequence)).limit(1).execute())[0];
24190
+ const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
24191
+ await exec.update(this.tables["t_sessions"]).set({
24192
+ fTitle: title ?? null,
24193
+ fTitleSeq: title === void 0 ? null : row.fSequence
24194
+ }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
24195
+ }
23635
24196
  async deleteBridgeTail(exec, id, fromSequence) {
23636
24197
  await exec.delete(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence))).execute();
23637
24198
  }
@@ -23824,6 +24385,7 @@ var SessionBranchRdbProvider = class {
23824
24385
  await tx.updateSeedLength(id, denseBoundary + 1);
23825
24386
  shrunk = denseBoundary + 1;
23826
24387
  }
24388
+ await tx.refreshTitle(id);
23827
24389
  await tx.bumpRevision(id);
23828
24390
  return shrunk;
23829
24391
  });
@@ -24641,7 +25203,476 @@ function adoptLegacyRows(row, eventRows) {
24641
25203
  };
24642
25204
  }
24643
25205
  //#endregion
25206
+ //#region ../../vendor/deepseek-harness/packages/storage/storage/lib/index.js
25207
+ /**
25208
+ * Error vocabulary for the storage hub and its backends.
25209
+ * @module @deepseek-ai/dsh-storage/src/error
25210
+ */
25211
+ /**
25212
+ * Error thrown by the hub and by backend implementations. The `code` is the
25213
+ * stable contract consumers may switch on; `message` is diagnostic prose.
25214
+ */
25215
+ var StorageError = class extends Error {
25216
+ code;
25217
+ name = "StorageError";
25218
+ /**
25219
+ * @param code - Stable discriminant for the failure class.
25220
+ * @param message - Human-readable diagnostic detail.
25221
+ * @param options - Standard error options (`cause`).
25222
+ */
25223
+ constructor(code, message, options) {
25224
+ super(message, options);
25225
+ this.code = code;
25226
+ }
25227
+ };
25228
+ /**
25229
+ * Storage hub (`ctx.storage`): a named backend registry plus mounted
25230
+ * data-form facilities. The hub itself performs no IO — backends own media,
25231
+ * data forms (the domain layer first) own semantics.
25232
+ * @module @deepseek-ai/dsh-storage
25233
+ */
25234
+ /**
25235
+ * Derive the Cordis lifecycle service that one named backend plugin provides.
25236
+ * Domain-form providers inject these keys so activation cannot race backend
25237
+ * registration even though callers continue resolving backends through the
25238
+ * storage registry.
25239
+ * @param name - Backend registry name.
25240
+ * @returns the corresponding lifecycle-only service key.
25241
+ */
25242
+ function storageBackendServiceKey(name) {
25243
+ return `storage.backend.${name}`;
25244
+ }
25245
+ //#endregion
25246
+ //#region ../session-rdb/src/storage-takeover/projection-cache.ts
25247
+ /**
25248
+ * 投影 checkpoint 服务(`ctx.sessionProjectionCache`)的 rdb 实现:替换上游
25249
+ * `@deepseek-ai/dsh-session-projection-cache` 插件,公开面与语义逐一对齐
25250
+ * (cachedSnapshot / cachedPredecessorTitle / hydratePrepared / write /
25251
+ * coldSnapshot,以及三个强制写点与计数/定时节流),持久层换成 session-rdb
25252
+ * 的语义专用表 `t_session_projcache` / `t_session_projcache_row`。
25253
+ *
25254
+ * 读方法是同步签名(session 列表在请求路径上直接调用),所以服务持有启动时
25255
+ * 从表中加载的内存 checkpoint 表;写先落库、后更新内存,保证读到的内存值
25256
+ * 磁盘上一定存在。
25257
+ */
25258
+ /** 服务注册名(与上游插件一致,消费者经 `ctx.get` 解析)。 */
25259
+ const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
25260
+ /** 只认 title 的前代提示(与上游一致)。 */
25261
+ const PREDECESSOR_TITLE_KEY = "title";
25262
+ /** rdb 持久化的投影 checkpoint 服务:同步驱动直读介质,异步驱动用写穿镜像支撑同步签名。 */
25263
+ var SessionProjectionCacheRdb = class extends Service {
25264
+ config;
25265
+ repository;
25266
+ ready;
25267
+ static inject = ["sessionProjections", "sessions"];
25268
+ /** 异步驱动(PostgreSQL)的同步读镜像:同步驱动(SQLite)直读介质,这里恒为空。 */
25269
+ records = /* @__PURE__ */ new Map();
25270
+ dirty = /* @__PURE__ */ new Map();
25271
+ /** 读路径是否直读介质(`readProjcacheSync` 存在即同步驱动)。 */
25272
+ directReads;
25273
+ /**
25274
+ * @param ctx - 插件上下文(须已注入 sessionProjections 与 sessions)。
25275
+ * @param config - 写节流参数。
25276
+ * @param repository - storages 接管表访问层。
25277
+ * @param ready - 介质就绪信号(直读介质前必须等到)。
25278
+ */
25279
+ constructor(ctx, config, repository, ready) {
25280
+ super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
25281
+ this.config = config;
25282
+ this.repository = repository;
25283
+ this.ready = ready;
25284
+ this.directReads = repository.readProjcacheSync !== void 0;
25285
+ }
25286
+ /**
25287
+ * Wait for the medium, then install the write path. Only the async driver
25288
+ * needs a startup mirror: the sync driver serves every read from the table.
25289
+ */
25290
+ async [Service.init]() {
25291
+ await this.ready;
25292
+ if (!this.directReads) for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
25293
+ this.installWritePath();
25294
+ }
25295
+ /** Read one session's stored record: straight from the medium, or from the async mirror. */
25296
+ lookup(id) {
25297
+ if (this.directReads) return this.repository.readProjcacheSync?.(id);
25298
+ return this.records.get(id);
25299
+ }
25300
+ /**
25301
+ * The stored record for one session, accepted only when its bound log
25302
+ * identity matches `expected`. A session id names a slot, not a lifecycle:
25303
+ * a recreated id or a persistence store swapped under a surviving cache
25304
+ * must not let an old record seed state folded from an unrelated log.
25305
+ * @param id - the session whose record is read.
25306
+ * @param expected - the log identity the caller holds (live or stored header).
25307
+ * @returns the identity-matching record, or `undefined` (absent or unrelated).
25308
+ */
25309
+ recordFor(id, expected) {
25310
+ const record = this.lookup(id);
25311
+ if (record === void 0) return void 0;
25312
+ return identityMatches(record.identity, expected) ? record : void 0;
25313
+ }
25314
+ /**
25315
+ * The cached projection cut for one stored (cold) or live header.
25316
+ * @param meta - authoritative Session header.
25317
+ * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
25318
+ * @param keys - optional projection keys required by the caller's audience.
25319
+ * @returns the cut (`asOfSeq` = lowest served-row watermark), or `undefined`
25320
+ * when no usable row exists for this lifecycle.
25321
+ */
25322
+ cachedSnapshot(meta, inheritedEventCount, keys) {
25323
+ const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
25324
+ const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
25325
+ return this.withDirectTitle(meta.id, keys, snapshot);
25326
+ }
25327
+ /**
25328
+ * 标题是会话数据本身(`t_sessions.f_title`,由 rdb 写路径与 rewind 维护):
25329
+ * checkpoint 行里没有 title 时直接取该列,列表消费不依赖缓存行是否存在。
25330
+ */
25331
+ withDirectTitle(id, keys, snapshot) {
25332
+ if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
25333
+ if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
25334
+ const direct = this.repository.readSessionTitleSync?.(id);
25335
+ if (direct === void 0) return snapshot;
25336
+ const values = {
25337
+ ...snapshot?.values,
25338
+ [PREDECESSOR_TITLE_KEY]: direct.title
25339
+ };
25340
+ return {
25341
+ asOfSeq: snapshot === void 0 ? direct.seq : Math.min(snapshot.asOfSeq, direct.seq),
25342
+ values
25343
+ };
25344
+ }
25345
+ /**
25346
+ * Read only a predecessor checkpoint's title as a zero-I/O listing hint.
25347
+ * @param meta - authoritative listed Session header.
25348
+ * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
25349
+ * @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
25350
+ * when the record is current, newer, unrelated, missing, or incompatible
25351
+ * with the title unit.
25352
+ */
25353
+ cachedPredecessorTitle(meta, inheritedEventCount) {
25354
+ const expected = identityOf(meta, inheritedEventCount);
25355
+ const record = this.lookup(meta.id);
25356
+ if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return;
25357
+ const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
25358
+ return title === void 0 ? void 0 : {
25359
+ ...title,
25360
+ asOfSeq: -1
25361
+ };
25362
+ }
25363
+ /** View selected wire rows and bind them to their lowest served watermark. */
25364
+ viewRecord(record, keys) {
25365
+ const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
25366
+ const servedKeys = Object.keys(values);
25367
+ if (servedKeys.length === 0) return void 0;
25368
+ const firstKey = servedKeys[0];
25369
+ let asOfSeq = record.rows[firstKey].seq;
25370
+ for (const key of servedKeys.slice(1)) {
25371
+ const row = record.rows[key];
25372
+ if (row.seq < asOfSeq) asOfSeq = row.seq;
25373
+ }
25374
+ return {
25375
+ asOfSeq,
25376
+ values
25377
+ };
25378
+ }
25379
+ /**
25380
+ * Hydrate projection cells for an already-prepared Session without another
25381
+ * persistence read. The cache seeds matching rows; the supplied exact log
25382
+ * advances every unit to the observation cut. No checkpoint is written
25383
+ * because the logical observation may contain recovery events not yet durable.
25384
+ * @param session - exact unpublished Session retained by persistence.
25385
+ * @param events - exact logical event prefix represented by the observation.
25386
+ * @returns all projection values at the event cut.
25387
+ */
25388
+ hydratePrepared(session, events) {
25389
+ const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
25390
+ if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
25391
+ try {
25392
+ return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
25393
+ } catch {
25394
+ return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
25395
+ }
25396
+ }
25397
+ /**
25398
+ * Durably checkpoint one live session NOW (all mandatory points call this).
25399
+ * NOT fail-soft — callers on the fail-soft paths contain it.
25400
+ * @param session - the live session to checkpoint.
25401
+ * @returns resolution after durability.
25402
+ */
25403
+ async write(session) {
25404
+ const rows = this.ctx.sessionProjections.checkpoint(session);
25405
+ this.markClean(session);
25406
+ if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
25407
+ await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
25408
+ }
25409
+ /**
25410
+ * Cold-read one session's projections from its complete log. Each unit is
25411
+ * seeded from the identity-checked cached rows and the refreshed checkpoint
25412
+ * is written back (fail-soft, fire-and-forget).
25413
+ * @param meta - the stored session header (identity witness).
25414
+ * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
25415
+ * @param events - the session's complete log, in seq order.
25416
+ * @returns the projection cut at the log end.
25417
+ */
25418
+ coldSnapshot(meta, inheritedEventCount, events) {
25419
+ const identity = identityOf(meta, inheritedEventCount);
25420
+ const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
25421
+ this.put(meta.id, identity, restored.checkpoint).catch((error) => {
25422
+ this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
25423
+ });
25424
+ return restored.snapshot;
25425
+ }
25426
+ installWritePath() {
25427
+ this.ctx.on("session/event", (session, event) => {
25428
+ if (event.type === "turn/end") {
25429
+ this.flushSoft(session, "turn/end");
25430
+ return;
25431
+ }
25432
+ const state = this.dirty.get(session) ?? {
25433
+ pending: 0,
25434
+ timer: void 0
25435
+ };
25436
+ this.dirty.set(session, state);
25437
+ state.pending += 1;
25438
+ if (state.pending >= this.config.writeEveryEvents) {
25439
+ this.flushSoft(session, "count threshold");
25440
+ return;
25441
+ }
25442
+ state.timer ??= setTimeout(() => {
25443
+ this.flushSoft(session, "interval");
25444
+ }, this.config.writeIntervalMs);
25445
+ });
25446
+ this.ctx.on("session/created", (session) => {
25447
+ this.flushSoft(session, "create");
25448
+ });
25449
+ this.ctx.on("session/disposed", (session) => {
25450
+ this.flushSoft(session, "detach");
25451
+ this.markClean(session);
25452
+ this.dirty.delete(session);
25453
+ });
25454
+ this.ctx.effect(() => () => {
25455
+ for (const state of this.dirty.values()) if (state.timer !== void 0) clearTimeout(state.timer);
25456
+ this.dirty.clear();
25457
+ }, "sessionProjectionCacheRdb.timers");
25458
+ }
25459
+ /** One fail-soft durable checkpoint. */
25460
+ async flushSoft(session, trigger) {
25461
+ try {
25462
+ await this.write(session);
25463
+ } catch (error) {
25464
+ this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
25465
+ }
25466
+ }
25467
+ /** Reset one session's dirty bookkeeping (its checkpoint is being written). */
25468
+ markClean(session) {
25469
+ const state = this.dirty.get(session);
25470
+ if (state === void 0) return;
25471
+ state.pending = 0;
25472
+ if (state.timer !== void 0) {
25473
+ clearTimeout(state.timer);
25474
+ state.timer = void 0;
25475
+ }
25476
+ }
25477
+ /** Replace one session's stored record with its log identity and a detached snapshot of `rows`. */
25478
+ async put(id, identity, rows) {
25479
+ const detached = detachJson(rows);
25480
+ await this.repository.putProjcache(id, detached);
25481
+ if (!this.directReads) this.records.set(id, {
25482
+ sessionId: id,
25483
+ identity,
25484
+ rows: detached
25485
+ });
25486
+ }
25487
+ };
25488
+ /** Detach one checkpoint from live unit state, refusing non-lossless JSON. */
25489
+ function detachJson(rows) {
25490
+ let text;
25491
+ try {
25492
+ text = JSON.stringify(rows);
25493
+ } catch (error) {
25494
+ throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
25495
+ }
25496
+ if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
25497
+ return JSON.parse(text);
25498
+ }
25499
+ /** Project a header onto the identity fields a record is bound to. */
25500
+ function identityOf(header, inheritedEventCount) {
25501
+ const cut = SessionLogOffset(inheritedEventCount);
25502
+ if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
25503
+ return {
25504
+ formatVersion: header.version,
25505
+ createdAt: header.createdAt,
25506
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
25507
+ isSeeded: header.isSeeded,
25508
+ inheritedEventCount: cut
25509
+ };
25510
+ }
25511
+ /**
25512
+ * Whether a stored record's bound identity names the caller's lifecycle.
25513
+ * An absent format generation cannot prove the fold semantics and never
25514
+ * matches. Once the format matches, absent lineage fields (records admitted
25515
+ * via compatible versions predate them) read as the unseeded lineage.
25516
+ */
25517
+ function identityMatches(stored, expected) {
25518
+ return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
25519
+ }
25520
+ /** Match one predecessor cache record to the authoritative listed lifecycle. */
25521
+ function predecessorIdentityMatches(stored, expected) {
25522
+ return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
25523
+ }
25524
+ /** Match the format-independent fields that distinguish one Session lifecycle. */
25525
+ function lifecycleIdentityMatches(stored, expected) {
25526
+ return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
25527
+ }
25528
+ //#endregion
25529
+ //#region ../session-rdb/src/storage-takeover/storage-backend.ts
25530
+ /**
25531
+ * storages 接管:注册在 storage hub 上的 `rdb` KV 后端。它把 workspace 域
25532
+ * (上游 single 布局:一张 `workspaces` 表 + global 单例)映射到 session-rdb
25533
+ * 的语义专用表,与事件日志同库同连接;域层(`ctx.storageDomain`)通过
25534
+ * `routes` 把 `workspace` 域路由到本后端。
25535
+ *
25536
+ * 本后端只服务已知域的专用表:未知 unit 名 fail loud,而不是静默落到通用
25537
+ * 存储——表结构是显式维护的(见 docs/schema.md)。
25538
+ */
25539
+ /** 本后端唯一服务的域:上游 `workspaceDomainSpec`(v2,single 布局)。 */
25540
+ const WORKSPACE_UNIT = "workspace";
25541
+ const WORKSPACE_TABLE = "workspaces";
25542
+ /** 只服务 workspace 域的 KV 后端。 */
25543
+ var RdbStorageBackend = class {
25544
+ repository;
25545
+ kv = { open: (descriptor) => this.openUnit(descriptor) };
25546
+ /** 已打开(或打开中)的 unit 名;重复 open 是调用方 bug。 */
25547
+ open = /* @__PURE__ */ new Set();
25548
+ closed = false;
25549
+ /**
25550
+ * @param repository - storages 接管表访问层(与事件日志同介质)。
25551
+ */
25552
+ constructor(repository) {
25553
+ this.repository = repository;
25554
+ }
25555
+ async openUnit(descriptor) {
25556
+ if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
25557
+ if (descriptor.name !== WORKSPACE_UNIT) throw new Error(`rdb storage backend serves only the '${WORKSPACE_UNIT}' domain (requested '${descriptor.name}')`);
25558
+ if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) throw new Error(`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape (table '${WORKSPACE_TABLE}' plus a global slot)`);
25559
+ if (this.open.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
25560
+ const stored = await this.repository.readUnitVersion(descriptor.name);
25561
+ if (stored === void 0) await this.repository.insertUnitVersion(descriptor.name, descriptor.version);
25562
+ else if (stored !== descriptor.version) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${stored} on the medium, incompatible with descriptor version ${descriptor.version}`);
25563
+ this.open.add(descriptor.name);
25564
+ return new WorkspaceKvUnit(this.repository, descriptor, () => {
25565
+ this.open.delete(descriptor.name);
25566
+ });
25567
+ }
25568
+ /**
25569
+ * Release the backend. The medium (the session database) belongs to the
25570
+ * owning session-rdb plugin, so closing open units here does not close it.
25571
+ * @returns resolution after the name table is cleared.
25572
+ */
25573
+ async close() {
25574
+ this.closed = true;
25575
+ this.open.clear();
25576
+ }
25577
+ };
25578
+ /** workspace 域的 KV unit:每个原语一条 SQL 语句,值形状与上游记录一致。 */
25579
+ var WorkspaceKvUnit = class {
25580
+ repository;
25581
+ descriptor;
25582
+ onClose;
25583
+ closed = false;
25584
+ constructor(repository, descriptor, onClose) {
25585
+ this.repository = repository;
25586
+ this.descriptor = descriptor;
25587
+ this.onClose = onClose;
25588
+ }
25589
+ async loadAll() {
25590
+ this.assertOpen();
25591
+ const records = Object.create(null);
25592
+ for (const { id, record } of await this.repository.listWorkspaces()) records[id] = record;
25593
+ const state = await this.repository.readWorkspaceState();
25594
+ return {
25595
+ tables: { [WORKSPACE_TABLE]: records },
25596
+ global: state
25597
+ };
25598
+ }
25599
+ async putRecord(table, key, value) {
25600
+ this.assertOpen();
25601
+ this.assertTable(table);
25602
+ await this.repository.putWorkspace(key, workspaceRecordOf(value));
25603
+ }
25604
+ async deleteRecord(table, key) {
25605
+ this.assertOpen();
25606
+ this.assertTable(table);
25607
+ await this.repository.deleteWorkspace(key);
25608
+ }
25609
+ async setGlobal(value) {
25610
+ this.assertOpen();
25611
+ await this.repository.writeWorkspaceState(workspaceStateOf(value));
25612
+ }
25613
+ async close() {
25614
+ if (this.closed) return;
25615
+ this.closed = true;
25616
+ this.onClose();
25617
+ }
25618
+ assertOpen() {
25619
+ if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
25620
+ }
25621
+ assertTable(table) {
25622
+ if (table !== WORKSPACE_TABLE) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
25623
+ }
25624
+ };
25625
+ /** Narrow one opaque KV record to the workspace record the domain shipped. */
25626
+ function workspaceRecordOf(value) {
25627
+ const record = value;
25628
+ if (typeof record !== "object" || record === null || typeof record.path !== "string" || typeof record.title !== "string" || !Array.isArray(record.sessionIds) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string") throw new TypeError("workspace record does not match the stored shape");
25629
+ return {
25630
+ path: record.path,
25631
+ title: record.title,
25632
+ sessionIds: record.sessionIds.map((id) => id),
25633
+ createdAt: record.createdAt,
25634
+ updatedAt: record.updatedAt
25635
+ };
25636
+ }
25637
+ /** Narrow one opaque KV global to the workspace registry state the domain shipped. */
25638
+ function workspaceStateOf(value) {
25639
+ const state = value;
25640
+ 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");
25641
+ return {
25642
+ initialized: state.initialized,
25643
+ workspaceIds: state.workspaceIds.map((id) => id),
25644
+ archivedSessionIds: state.archivedSessionIds.map((id) => id),
25645
+ ...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
25646
+ };
25647
+ }
25648
+ //#endregion
25649
+ //#region ../session-rdb/src/storage-takeover/index.ts
25650
+ /**
25651
+ * Install the storages takeover on the session-rdb plugin context.
25652
+ * @param ctx - session-rdb plugin context.
25653
+ * @param options - repository and projection-cache throttle parameters.
25654
+ */
25655
+ function installStorageTakeover(ctx, options) {
25656
+ ctx.inject(["storage"], (storageCtx) => {
25657
+ const backend = new RdbStorageBackend(options.repository);
25658
+ storageCtx.effect(() => {
25659
+ const unregister = storageCtx.storage.backend.register("rdb", backend);
25660
+ return async () => {
25661
+ unregister();
25662
+ await backend.close();
25663
+ };
25664
+ }, "session-rdb.storageBackend");
25665
+ storageCtx.provide(storageBackendServiceKey("rdb"), backend);
25666
+ });
25667
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
25668
+ new SessionProjectionCacheRdb(projectionCtx, options.projectionCache, options.repository, options.ready);
25669
+ });
25670
+ }
25671
+ //#endregion
24644
25672
  //#region ../session-rdb/src/index.ts
25673
+ /** 投影缓存默认写节流(与上游 base 装配的部署值一致)。 */
25674
+ const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
25675
+ const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
24645
25676
  /** 会话级写所有权与 live 路由簿记。 */
24646
25677
  var RdbBackendTracker = class {
24647
25678
  name;
@@ -24910,11 +25941,25 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
24910
25941
  "truncate",
24911
25942
  "persist"
24912
25943
  ]).default("wal"),
24913
- busyTimeout: Schema.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS)
25944
+ busyTimeout: Schema.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS),
25945
+ projectionCache: Schema.object({
25946
+ writeEveryEvents: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
25947
+ writeIntervalMs: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
25948
+ }).default({
25949
+ writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
25950
+ writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
25951
+ })
24914
25952
  }), Schema.object({
24915
25953
  type: Schema.const("postgres"),
24916
25954
  connectionString: Schema.string().required(),
24917
- schema: Schema.string().default("public")
25955
+ schema: Schema.string().default("public"),
25956
+ projectionCache: Schema.object({
25957
+ writeEveryEvents: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
25958
+ writeIntervalMs: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
25959
+ }).default({
25960
+ writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
25961
+ writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
25962
+ })
24918
25963
  })]);
24919
25964
  static settingsNs = "session-rdb";
24920
25965
  name = "session-rdb";
@@ -24945,6 +25990,14 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
24945
25990
  this.installLiveRouting(ctx);
24946
25991
  new SessionBranchRdb(this.ctx);
24947
25992
  registerSessionImport(this.ctx, this);
25993
+ installStorageTakeover(this.ctx, {
25994
+ repository: this.backend.storage,
25995
+ ready: this.ready,
25996
+ projectionCache: {
25997
+ writeEveryEvents: this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
25998
+ writeIntervalMs: this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS
25999
+ }
26000
+ });
24948
26001
  }
24949
26002
  async init() {
24950
26003
  await this.backend.open();
@@ -25455,6 +26508,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
25455
26508
  }
25456
26509
  if (eventRows.length > 0) await tx.insertEvents(eventRows);
25457
26510
  await tx.insertBridges(bridgeRows);
26511
+ if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
25458
26512
  return {
25459
26513
  headEventId: parentId,
25460
26514
  headSequence: nextSeq - 1