@morlay/session-rdb 0.0.16-alpha.4 → 0.0.17
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/README.md +44 -10
- package/dist/artifact.d.mts +9 -2
- package/dist/artifact.mjs +3 -3
- package/dist/{import-BPEHfHNk.mjs → import-DZIAjOUr.mjs} +2 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-CgAqCQAb.d.mts → index-BEKoV1Uy.d.mts} +11 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +509 -6
- package/dist/{log-BIuXv09P.mjs → log-DO69NQnn.mjs} +16 -1
- package/dist/schema-Bo6Hy5gW.d.mts +3476 -0
- package/dist/{sqlite-IH5I48aL.mjs → sqlite-BIPdMNQb.mjs} +504 -6
- package/dist/storage.d.mts +10 -2
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +65 -0
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +1236 -0
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +10 -0
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +1236 -0
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +66 -0
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +958 -0
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +13 -0
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +958 -0
- package/package.json +20 -18
- package/src/backend.ts +7 -0
- package/src/branch.ts +2 -0
- package/src/drizzle/postgres-v3.ts +5 -0
- package/src/drizzle/sqlite-v3.ts +5 -0
- package/src/entities/v3/index.ts +20 -0
- package/src/entities/v3/session-projcache-rows.ts +27 -0
- package/src/entities/v3/sessions.ts +6 -0
- package/src/entities/v3/storage-units.ts +10 -0
- package/src/entities/v3/workspace-sessions.ts +24 -0
- package/src/entities/v3/workspace-state.ts +18 -0
- package/src/entities/v3/workspaces.ts +19 -0
- package/src/import-storages.ts +173 -0
- package/src/index.ts +53 -0
- package/src/log.ts +19 -0
- package/src/postgres.ts +70 -2
- package/src/schema.ts +16 -2
- package/src/sqlite.ts +87 -4
- package/src/storage-takeover/index.ts +60 -0
- package/src/storage-takeover/projection-cache.ts +445 -0
- package/src/storage-takeover/repository.ts +420 -0
- package/src/storage-takeover/storage-backend.ts +177 -0
- package/src/storage-takeover/types.ts +82 -0
- package/dist/schema-COK7-wRV.d.mts +0 -104
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { m as sessionInsertRow, p as sessionConflictRow } from "./log-
|
|
1
|
+
import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow } from "./log-DO69NQnn.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { and, desc, eq, gte, isNotNull, notInArray, sql } from "drizzle-orm";
|
|
4
5
|
import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
5
6
|
import { bigint, check as check$1, index as index$1, integer as integer$1, pgSchema, pgTable, serial, text as text$1, unique as unique$1 } from "drizzle-orm/pg-core";
|
|
6
7
|
import { readdirSync, statSync } from "node:fs";
|
|
@@ -196,7 +197,13 @@ const sessions = {
|
|
|
196
197
|
f_revision: {
|
|
197
198
|
type: "integer",
|
|
198
199
|
notNull: true
|
|
199
|
-
}
|
|
200
|
+
},
|
|
201
|
+
/** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
|
|
202
|
+
f_archived_at: { type: "bigint" },
|
|
203
|
+
/** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
|
|
204
|
+
f_title: { type: "text" },
|
|
205
|
+
/** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
|
|
206
|
+
f_title_seq: { type: "integer" }
|
|
200
207
|
}
|
|
201
208
|
};
|
|
202
209
|
//#endregion
|
|
@@ -302,20 +309,193 @@ const sessionEvents$1 = {
|
|
|
302
309
|
indexes: { idx_session_events_event_id: { columns: ["f_event_id"] } }
|
|
303
310
|
};
|
|
304
311
|
//#endregion
|
|
312
|
+
//#region src/entities/v3/storage-units.ts
|
|
313
|
+
/** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
|
|
314
|
+
const storageUnits = {
|
|
315
|
+
name: "t_storage_units",
|
|
316
|
+
columns: {
|
|
317
|
+
f_name: {
|
|
318
|
+
type: "text",
|
|
319
|
+
primaryKey: true
|
|
320
|
+
},
|
|
321
|
+
f_version: {
|
|
322
|
+
type: "integer",
|
|
323
|
+
notNull: true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/entities/v3/workspaces.ts
|
|
329
|
+
/**
|
|
330
|
+
* workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
|
|
331
|
+
* 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
|
|
332
|
+
* 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
|
|
333
|
+
*/
|
|
334
|
+
const workspaces = {
|
|
335
|
+
name: "t_workspaces",
|
|
336
|
+
columns: {
|
|
337
|
+
f_id: {
|
|
338
|
+
type: "serial",
|
|
339
|
+
primaryKey: true
|
|
340
|
+
},
|
|
341
|
+
f_workspace_id: {
|
|
342
|
+
type: "text",
|
|
343
|
+
notNull: true,
|
|
344
|
+
unique: true
|
|
345
|
+
},
|
|
346
|
+
f_path: {
|
|
347
|
+
type: "text",
|
|
348
|
+
notNull: true
|
|
349
|
+
},
|
|
350
|
+
f_title: {
|
|
351
|
+
type: "text",
|
|
352
|
+
notNull: true
|
|
353
|
+
},
|
|
354
|
+
f_created_at: {
|
|
355
|
+
type: "text",
|
|
356
|
+
notNull: true
|
|
357
|
+
},
|
|
358
|
+
f_updated_at: {
|
|
359
|
+
type: "text",
|
|
360
|
+
notNull: true
|
|
361
|
+
},
|
|
362
|
+
f_position: {
|
|
363
|
+
type: "integer",
|
|
364
|
+
notNull: true,
|
|
365
|
+
default: -1
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/entities/v3/workspace-sessions.ts
|
|
371
|
+
/**
|
|
372
|
+
* workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
|
|
373
|
+
* 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
|
|
374
|
+
* session 反查归属 workspace。
|
|
375
|
+
*/
|
|
376
|
+
const workspaceSessions = {
|
|
377
|
+
name: "t_workspace_sessions",
|
|
378
|
+
columns: {
|
|
379
|
+
f_id: {
|
|
380
|
+
type: "serial",
|
|
381
|
+
primaryKey: true
|
|
382
|
+
},
|
|
383
|
+
f_workspace_id: {
|
|
384
|
+
type: "text",
|
|
385
|
+
notNull: true,
|
|
386
|
+
references: {
|
|
387
|
+
table: "t_workspaces",
|
|
388
|
+
column: "f_workspace_id",
|
|
389
|
+
onDelete: "cascade"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
f_session_id: {
|
|
393
|
+
type: "text",
|
|
394
|
+
notNull: true
|
|
395
|
+
},
|
|
396
|
+
f_position: {
|
|
397
|
+
type: "integer",
|
|
398
|
+
notNull: true
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
uniques: { uq_workspace_sessions_workspace_session: { columns: ["f_workspace_id", "f_session_id"] } },
|
|
402
|
+
indexes: { idx_workspace_sessions_session_id: { columns: ["f_session_id"] } }
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/entities/v3/workspace-state.ts
|
|
406
|
+
/**
|
|
407
|
+
* workspace 域 global 单例(上游 `workspaceDomainState`):`initialized` 是
|
|
408
|
+
* 引导标记,`pendingMutation` 拆成操作与目标两列(create / delete 的可恢复
|
|
409
|
+
* 两写标记)。显示顺序(`workspaceIds`)在 `t_workspaces.f_position`,
|
|
410
|
+
* 归档集在 `t_session_archives`。
|
|
411
|
+
*/
|
|
412
|
+
const workspaceState = {
|
|
413
|
+
name: "t_workspace_state",
|
|
414
|
+
columns: {
|
|
415
|
+
f_singleton: {
|
|
416
|
+
type: "integer",
|
|
417
|
+
primaryKey: true
|
|
418
|
+
},
|
|
419
|
+
f_initialized: {
|
|
420
|
+
type: "integer",
|
|
421
|
+
notNull: true
|
|
422
|
+
},
|
|
423
|
+
f_pending_operation: { type: "text" },
|
|
424
|
+
f_pending_workspace_id: { type: "text" }
|
|
425
|
+
},
|
|
426
|
+
checks: { ck_workspace_state_singleton: { expression: "f_singleton = 1" } }
|
|
427
|
+
};
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/entities/v3/session-projcache-rows.ts
|
|
430
|
+
/**
|
|
431
|
+
* 投影 checkpoint 行(上游 checkpoint 的 `(sessionId, key, ver, seq, val)`
|
|
432
|
+
* 逐行落库):`f_seq` 是该行的日志水位、`f_ver` 是投影单元的 stateVersion。
|
|
433
|
+
* 行是折出捷径、非权威,版本不匹配时整行丢弃。
|
|
434
|
+
*
|
|
435
|
+
* checkpoint 的日志 identity 不另存一份:它与 `t_sessions` 的行是 1:1,直接
|
|
436
|
+
* 复用该行的 `f_version` / `f_created_at` / `f_cwd` / `f_seed_length` 做校验。
|
|
437
|
+
*/
|
|
438
|
+
const sessionProjcacheRows = {
|
|
439
|
+
name: "t_session_projcache_row",
|
|
440
|
+
columns: {
|
|
441
|
+
f_id: {
|
|
442
|
+
type: "serial",
|
|
443
|
+
primaryKey: true
|
|
444
|
+
},
|
|
445
|
+
f_session_id: {
|
|
446
|
+
type: "text",
|
|
447
|
+
notNull: true,
|
|
448
|
+
references: {
|
|
449
|
+
table: "t_sessions",
|
|
450
|
+
column: "f_session_id",
|
|
451
|
+
onDelete: "cascade"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
f_key: {
|
|
455
|
+
type: "text",
|
|
456
|
+
notNull: true
|
|
457
|
+
},
|
|
458
|
+
f_ver: {
|
|
459
|
+
type: "integer",
|
|
460
|
+
notNull: true
|
|
461
|
+
},
|
|
462
|
+
f_seq: {
|
|
463
|
+
type: "integer",
|
|
464
|
+
notNull: true
|
|
465
|
+
},
|
|
466
|
+
f_val: {
|
|
467
|
+
type: "text",
|
|
468
|
+
notNull: true
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
uniques: { uq_session_projcache_row_session_key: { columns: ["f_session_id", "f_key"] } },
|
|
472
|
+
indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
|
|
473
|
+
};
|
|
474
|
+
//#endregion
|
|
305
475
|
//#region src/entities/v3/index.ts
|
|
306
476
|
const sqliteTableDefs = [
|
|
307
477
|
persistenceState,
|
|
308
478
|
schemaMeta,
|
|
309
479
|
sessions,
|
|
310
480
|
events,
|
|
311
|
-
sessionEvents$1
|
|
481
|
+
sessionEvents$1,
|
|
482
|
+
storageUnits,
|
|
483
|
+
workspaces,
|
|
484
|
+
workspaceSessions,
|
|
485
|
+
workspaceState,
|
|
486
|
+
sessionProjcacheRows
|
|
312
487
|
];
|
|
313
488
|
const postgresTableDefs = [
|
|
314
489
|
persistenceState,
|
|
315
490
|
schemaMeta,
|
|
316
491
|
sessions,
|
|
317
492
|
events,
|
|
318
|
-
sessionEvents$1
|
|
493
|
+
sessionEvents$1,
|
|
494
|
+
storageUnits,
|
|
495
|
+
workspaces,
|
|
496
|
+
workspaceSessions,
|
|
497
|
+
workspaceState,
|
|
498
|
+
sessionProjcacheRows
|
|
319
499
|
];
|
|
320
500
|
({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
|
|
321
501
|
//#endregion
|
|
@@ -329,6 +509,11 @@ const tSchemaMeta = sqliteTables["t_schema_meta"];
|
|
|
329
509
|
const tSessions = sqliteTables["t_sessions"];
|
|
330
510
|
const tEvents = sqliteTables["t_events"];
|
|
331
511
|
const tSessionEvents = sqliteTables["t_session_events"];
|
|
512
|
+
const tStorageUnits = sqliteTables["t_storage_units"];
|
|
513
|
+
const tWorkspaces = sqliteTables["t_workspaces"];
|
|
514
|
+
const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
|
|
515
|
+
const tWorkspaceState = sqliteTables["t_workspace_state"];
|
|
516
|
+
const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
|
|
332
517
|
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
333
518
|
function eventKind(event) {
|
|
334
519
|
switch (event.type) {
|
|
@@ -343,6 +528,8 @@ function eventKind(event) {
|
|
|
343
528
|
case "session/end-seed": return "turn";
|
|
344
529
|
case "tool/call":
|
|
345
530
|
case "tool/result":
|
|
531
|
+
case "tool/ptc-dispatch-start":
|
|
532
|
+
case "tool/ptc-dispatch":
|
|
346
533
|
case "tool/code-dispatch-start":
|
|
347
534
|
case "tool/code-dispatch": return "tool";
|
|
348
535
|
case "request/header":
|
|
@@ -416,6 +603,8 @@ function eventDimensions(event) {
|
|
|
416
603
|
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
417
604
|
actionId: typeof data["callId"] === "string" ? data["callId"] : ""
|
|
418
605
|
};
|
|
606
|
+
case "tool/ptc-dispatch-start":
|
|
607
|
+
case "tool/ptc-dispatch":
|
|
419
608
|
case "tool/code-dispatch-start":
|
|
420
609
|
case "tool/code-dispatch": return {
|
|
421
610
|
kind,
|
|
@@ -481,6 +670,254 @@ function eventDimensions(event) {
|
|
|
481
670
|
}
|
|
482
671
|
}
|
|
483
672
|
//#endregion
|
|
673
|
+
//#region src/storage-takeover/repository.ts
|
|
674
|
+
/**
|
|
675
|
+
* storages 接管表的方言无关实现:SQLite 与 PostgreSQL 的 drizzle 查询构建器
|
|
676
|
+
* 共享同一套调用形状(SQLite 走同步 `.all()`/`.get()`/`.run()`,PostgreSQL
|
|
677
|
+
* 走 thenable),差别由本模块的执行 helper 吸收。
|
|
678
|
+
*
|
|
679
|
+
* workspace 域是权威数据:记录、归属、显示顺序与归档集都拆成语义表,每次写入
|
|
680
|
+
* 在介质事务内完成(`host.writeAtomically`),不留部分应用的中间态。
|
|
681
|
+
*/
|
|
682
|
+
/** 多行查询:SQLite 同步 `.all()`,PostgreSQL await。 */
|
|
683
|
+
async function allRows(query) {
|
|
684
|
+
const q = query;
|
|
685
|
+
if (typeof q.all === "function") return q.all();
|
|
686
|
+
return await query;
|
|
687
|
+
}
|
|
688
|
+
/** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
|
|
689
|
+
async function oneRow(query) {
|
|
690
|
+
const q = query;
|
|
691
|
+
if (typeof q.get === "function") return q.get();
|
|
692
|
+
return (await query)[0];
|
|
693
|
+
}
|
|
694
|
+
/** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
|
|
695
|
+
async function runQuery(query) {
|
|
696
|
+
const q = query;
|
|
697
|
+
if (typeof q.run === "function") {
|
|
698
|
+
q.run();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
await query;
|
|
702
|
+
}
|
|
703
|
+
/** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
|
|
704
|
+
function identityOfSession(row) {
|
|
705
|
+
return {
|
|
706
|
+
formatVersion: row.fVersion,
|
|
707
|
+
createdAt: row.fCreatedAt,
|
|
708
|
+
...row.fCwd === null ? {} : { cwd: row.fCwd },
|
|
709
|
+
isSeeded: row.fSeedLength !== null,
|
|
710
|
+
...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
|
|
714
|
+
function seqCursor(seq) {
|
|
715
|
+
return seq === -1 ? -1 : SessionSeq(seq);
|
|
716
|
+
}
|
|
717
|
+
/** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
|
|
718
|
+
function pendingMutationOf(row) {
|
|
719
|
+
const operation = row.fPendingOperation;
|
|
720
|
+
if (operation !== "create" && operation !== "delete") return void 0;
|
|
721
|
+
return {
|
|
722
|
+
operation,
|
|
723
|
+
workspaceId: row.fPendingWorkspaceId ?? ""
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* 构建一个介质的 storages 接管访问层。
|
|
728
|
+
* @param host - 介质句柄、方言表对象与事务执行器。
|
|
729
|
+
* @returns 方言无关的访问层。
|
|
730
|
+
*/
|
|
731
|
+
function createStorageRepository(host) {
|
|
732
|
+
const tables = host.tables;
|
|
733
|
+
const tUnits = tables["t_storage_units"];
|
|
734
|
+
const tSessions = tables["t_sessions"];
|
|
735
|
+
const tWorkspaces = tables["t_workspaces"];
|
|
736
|
+
const tWorkspaceSessions = tables["t_workspace_sessions"];
|
|
737
|
+
const tWorkspaceState = tables["t_workspace_state"];
|
|
738
|
+
const tProjcacheRows = tables["t_session_projcache_row"];
|
|
739
|
+
/** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
|
|
740
|
+
const sessionIdentity = {
|
|
741
|
+
fSessionId: tSessions.fSessionId,
|
|
742
|
+
fVersion: tSessions.fVersion,
|
|
743
|
+
fCreatedAt: tSessions.fCreatedAt,
|
|
744
|
+
fCwd: tSessions.fCwd,
|
|
745
|
+
fSeedLength: tSessions.fSeedLength
|
|
746
|
+
};
|
|
747
|
+
const dbx = async () => await host.db();
|
|
748
|
+
return {
|
|
749
|
+
async readUnitVersion(name) {
|
|
750
|
+
return (await oneRow((await dbx()).select().from(tUnits).where(eq(tUnits.fName, name))))?.fVersion;
|
|
751
|
+
},
|
|
752
|
+
async insertUnitVersion(name, version) {
|
|
753
|
+
await runQuery((await dbx()).insert(tUnits).values({
|
|
754
|
+
fName: name,
|
|
755
|
+
fVersion: version
|
|
756
|
+
}).onConflictDoNothing());
|
|
757
|
+
},
|
|
758
|
+
async listWorkspaces() {
|
|
759
|
+
const db = await dbx();
|
|
760
|
+
const rows = await allRows(db.select().from(tWorkspaces));
|
|
761
|
+
const links = await allRows(db.select().from(tWorkspaceSessions));
|
|
762
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
763
|
+
for (const link of links) {
|
|
764
|
+
const owned = sessions.get(link.fWorkspaceId) ?? [];
|
|
765
|
+
owned.push({
|
|
766
|
+
id: link.fSessionId,
|
|
767
|
+
position: link.fPosition
|
|
768
|
+
});
|
|
769
|
+
sessions.set(link.fWorkspaceId, owned);
|
|
770
|
+
}
|
|
771
|
+
return rows.map((row) => ({
|
|
772
|
+
id: row.fWorkspaceId,
|
|
773
|
+
record: {
|
|
774
|
+
path: row.fPath,
|
|
775
|
+
title: row.fTitle,
|
|
776
|
+
sessionIds: (sessions.get(row.fWorkspaceId) ?? []).sort((left, right) => left.position - right.position).map((entry) => SessionId(entry.id)),
|
|
777
|
+
createdAt: row.fCreatedAt,
|
|
778
|
+
updatedAt: row.fUpdatedAt
|
|
779
|
+
}
|
|
780
|
+
}));
|
|
781
|
+
},
|
|
782
|
+
async putWorkspace(id, record) {
|
|
783
|
+
await host.writeAtomically(async () => {
|
|
784
|
+
const db = await dbx();
|
|
785
|
+
const values = {
|
|
786
|
+
fWorkspaceId: id,
|
|
787
|
+
fPath: record.path,
|
|
788
|
+
fTitle: record.title,
|
|
789
|
+
fCreatedAt: record.createdAt,
|
|
790
|
+
fUpdatedAt: record.updatedAt
|
|
791
|
+
};
|
|
792
|
+
await runQuery(db.insert(tWorkspaces).values(values).onConflictDoUpdate({
|
|
793
|
+
target: tWorkspaces.fWorkspaceId,
|
|
794
|
+
set: values
|
|
795
|
+
}));
|
|
796
|
+
await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
|
|
797
|
+
const links = record.sessionIds.map((sessionId, position) => ({
|
|
798
|
+
fWorkspaceId: id,
|
|
799
|
+
fSessionId: sessionId,
|
|
800
|
+
fPosition: position
|
|
801
|
+
}));
|
|
802
|
+
if (links.length > 0) await runQuery(db.insert(tWorkspaceSessions).values(links));
|
|
803
|
+
});
|
|
804
|
+
},
|
|
805
|
+
async deleteWorkspace(id) {
|
|
806
|
+
await host.writeAtomically(async () => {
|
|
807
|
+
const db = await dbx();
|
|
808
|
+
await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
|
|
809
|
+
await runQuery(db.delete(tWorkspaces).where(eq(tWorkspaces.fWorkspaceId, id)));
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
async readWorkspaceState() {
|
|
813
|
+
const db = await dbx();
|
|
814
|
+
const row = await oneRow(db.select().from(tWorkspaceState).where(eq(tWorkspaceState.fSingleton, 1)));
|
|
815
|
+
if (row === void 0) return null;
|
|
816
|
+
const ordered = await allRows(db.select().from(tWorkspaces).where(gte(tWorkspaces.fPosition, 0)).orderBy(tWorkspaces.fPosition));
|
|
817
|
+
const archives = await allRows(db.select({ fSessionId: tSessions.fSessionId }).from(tSessions).where(isNotNull(tSessions.fArchivedAt)).orderBy(tSessions.fArchivedAt));
|
|
818
|
+
const pendingMutation = pendingMutationOf(row);
|
|
819
|
+
return {
|
|
820
|
+
initialized: row.fInitialized !== 0,
|
|
821
|
+
workspaceIds: ordered.map((entry) => entry.fWorkspaceId),
|
|
822
|
+
archivedSessionIds: archives.map((entry) => entry.fSessionId),
|
|
823
|
+
...pendingMutation === void 0 ? {} : { pendingMutation }
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
async writeWorkspaceState(state) {
|
|
827
|
+
await host.writeAtomically(async () => {
|
|
828
|
+
const db = await dbx();
|
|
829
|
+
const pending = state.pendingMutation;
|
|
830
|
+
const values = {
|
|
831
|
+
fSingleton: 1,
|
|
832
|
+
fInitialized: state.initialized ? 1 : 0,
|
|
833
|
+
fPendingOperation: pending === void 0 || typeof pending.operation !== "string" ? null : pending.operation,
|
|
834
|
+
fPendingWorkspaceId: pending === void 0 || typeof pending.workspaceId !== "string" ? null : pending.workspaceId
|
|
835
|
+
};
|
|
836
|
+
await runQuery(db.insert(tWorkspaceState).values(values).onConflictDoUpdate({
|
|
837
|
+
target: tWorkspaceState.fSingleton,
|
|
838
|
+
set: values
|
|
839
|
+
}));
|
|
840
|
+
await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
|
|
841
|
+
for (const [position, workspaceId] of state.workspaceIds.entries()) await runQuery(db.update(tWorkspaces).set({ fPosition: position }).where(eq(tWorkspaces.fWorkspaceId, workspaceId)));
|
|
842
|
+
const archived = state.archivedSessionIds;
|
|
843
|
+
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))));
|
|
844
|
+
const stamp = Date.now();
|
|
845
|
+
for (const sessionId of archived) await runQuery(db.update(tSessions).set({ fArchivedAt: sql`COALESCE(${tSessions.fArchivedAt}, ${stamp})` }).where(eq(tSessions.fSessionId, sessionId)));
|
|
846
|
+
});
|
|
847
|
+
},
|
|
848
|
+
async loadProjcache() {
|
|
849
|
+
const db = await dbx();
|
|
850
|
+
const rows = await allRows(db.select().from(tProjcacheRows));
|
|
851
|
+
if (rows.length === 0) return [];
|
|
852
|
+
const sessions = await allRows(db.select(sessionIdentity).from(tSessions));
|
|
853
|
+
const entries = /* @__PURE__ */ new Map();
|
|
854
|
+
for (const session of sessions) entries.set(session.fSessionId, {
|
|
855
|
+
sessionId: SessionId(session.fSessionId),
|
|
856
|
+
identity: identityOfSession(session),
|
|
857
|
+
rows: {}
|
|
858
|
+
});
|
|
859
|
+
for (const row of rows) {
|
|
860
|
+
const entry = entries.get(row.fSessionId);
|
|
861
|
+
if (entry === void 0) continue;
|
|
862
|
+
entry.rows[row.fKey] = {
|
|
863
|
+
ver: row.fVer,
|
|
864
|
+
seq: seqCursor(row.fSeq),
|
|
865
|
+
val: JSON.parse(row.fVal)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
|
|
869
|
+
},
|
|
870
|
+
async putProjcache(sessionId, rows) {
|
|
871
|
+
await host.writeAtomically(async () => {
|
|
872
|
+
const db = await dbx();
|
|
873
|
+
await runQuery(db.delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
|
|
874
|
+
const values = Object.entries(rows).map(([key, row]) => ({
|
|
875
|
+
fSessionId: sessionId,
|
|
876
|
+
fKey: key,
|
|
877
|
+
fVer: row.ver,
|
|
878
|
+
fSeq: row.seq,
|
|
879
|
+
fVal: JSON.stringify(row.val)
|
|
880
|
+
}));
|
|
881
|
+
if (values.length > 0) await runQuery(db.insert(tProjcacheRows).values(values));
|
|
882
|
+
});
|
|
883
|
+
},
|
|
884
|
+
async deleteProjcache(sessionId) {
|
|
885
|
+
await runQuery((await dbx()).delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
|
|
886
|
+
},
|
|
887
|
+
...host.dbSync === void 0 ? {} : {
|
|
888
|
+
readProjcacheSync: (sessionId) => {
|
|
889
|
+
const db = host.dbSync();
|
|
890
|
+
const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
|
|
891
|
+
if (stored.length === 0) return void 0;
|
|
892
|
+
const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
893
|
+
if (session === void 0) return void 0;
|
|
894
|
+
const rows = {};
|
|
895
|
+
for (const row of stored) rows[row.fKey] = {
|
|
896
|
+
ver: row.fVer,
|
|
897
|
+
seq: seqCursor(row.fSeq),
|
|
898
|
+
val: JSON.parse(row.fVal)
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
sessionId: SessionId(sessionId),
|
|
902
|
+
identity: identityOfSession(session),
|
|
903
|
+
rows
|
|
904
|
+
};
|
|
905
|
+
},
|
|
906
|
+
readSessionTitleSync: (sessionId) => {
|
|
907
|
+
const row = host.dbSync().select({
|
|
908
|
+
fTitle: tSessions.fTitle,
|
|
909
|
+
fTitleSeq: tSessions.fTitleSeq
|
|
910
|
+
}).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
911
|
+
if (row === void 0 || row.fTitle === null || row.fTitleSeq === null) return void 0;
|
|
912
|
+
return {
|
|
913
|
+
title: row.fTitle,
|
|
914
|
+
seq: row.fTitleSeq
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
//#endregion
|
|
484
921
|
//#region src/sqlite.ts
|
|
485
922
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
486
923
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
@@ -555,12 +992,60 @@ var SqliteBackend = class SqliteBackend {
|
|
|
555
992
|
options;
|
|
556
993
|
kind = "sqlite";
|
|
557
994
|
storeIdentity;
|
|
995
|
+
/** storages 接管表访问层;句柄在 open() 完成后解析。 */
|
|
996
|
+
storage;
|
|
558
997
|
dbPath = "";
|
|
559
998
|
db;
|
|
999
|
+
dbReady;
|
|
1000
|
+
resolveDb;
|
|
1001
|
+
rejectDb;
|
|
560
1002
|
constructor(options) {
|
|
561
1003
|
this.options = options;
|
|
1004
|
+
this.dbReady = new Promise((resolve, reject) => {
|
|
1005
|
+
this.resolveDb = resolve;
|
|
1006
|
+
this.rejectDb = reject;
|
|
1007
|
+
});
|
|
1008
|
+
this.dbReady.catch(() => {});
|
|
1009
|
+
this.storage = createStorageRepository({
|
|
1010
|
+
db: () => this.dbReady,
|
|
1011
|
+
dbSync: () => {
|
|
1012
|
+
if (this.db === void 0) throw new Error("sqlite session database is not open");
|
|
1013
|
+
return this.db;
|
|
1014
|
+
},
|
|
1015
|
+
writeAtomically: (fn) => enqueueSqliteTx(this.dbPath, async () => {
|
|
1016
|
+
const db = await this.dbReady;
|
|
1017
|
+
db.$client.exec("BEGIN IMMEDIATE");
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await fn();
|
|
1020
|
+
db.$client.exec("COMMIT");
|
|
1021
|
+
return result;
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
try {
|
|
1024
|
+
db.$client.exec("ROLLBACK");
|
|
1025
|
+
} catch {}
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
1028
|
+
}),
|
|
1029
|
+
tables: {
|
|
1030
|
+
t_sessions: tSessions,
|
|
1031
|
+
t_storage_units: tStorageUnits,
|
|
1032
|
+
t_workspaces: tWorkspaces,
|
|
1033
|
+
t_workspace_sessions: tWorkspaceSessions,
|
|
1034
|
+
t_workspace_state: tWorkspaceState,
|
|
1035
|
+
t_session_projcache_row: tSessionProjcacheRows
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
562
1038
|
}
|
|
563
1039
|
async open() {
|
|
1040
|
+
try {
|
|
1041
|
+
await this.doOpen();
|
|
1042
|
+
this.resolveDb(this.db);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
this.rejectDb(error);
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async doOpen() {
|
|
564
1049
|
const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
|
|
565
1050
|
this.dbPath = actual;
|
|
566
1051
|
if (actual !== ":memory:") {
|
|
@@ -626,6 +1111,7 @@ var SqliteBackend = class SqliteBackend {
|
|
|
626
1111
|
insertBridges: (rows) => this.insertBridges(rows),
|
|
627
1112
|
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
628
1113
|
bumpRevision: (id) => this.bumpRevision(id),
|
|
1114
|
+
refreshTitle: (id) => this.refreshTitle(id),
|
|
629
1115
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
630
1116
|
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
|
|
631
1117
|
};
|
|
@@ -671,6 +1157,18 @@ var SqliteBackend = class SqliteBackend {
|
|
|
671
1157
|
async bumpRevision(id) {
|
|
672
1158
|
this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
|
|
673
1159
|
}
|
|
1160
|
+
/** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
|
|
1161
|
+
async refreshTitle(id) {
|
|
1162
|
+
const row = this.db.select({
|
|
1163
|
+
fSequence: tSessionEvents.fSequence,
|
|
1164
|
+
fData: tEvents.fData
|
|
1165
|
+
}).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();
|
|
1166
|
+
const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
|
|
1167
|
+
this.db.update(tSessions).set({
|
|
1168
|
+
fTitle: title ?? null,
|
|
1169
|
+
fTitleSeq: title === void 0 ? null : row.fSequence
|
|
1170
|
+
}).where(eq(tSessions.fSessionId, id)).run();
|
|
1171
|
+
}
|
|
674
1172
|
async deleteBridgeTail(id, fromSequence) {
|
|
675
1173
|
this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
|
|
676
1174
|
}
|
|
@@ -696,4 +1194,4 @@ var SqliteBackend = class SqliteBackend {
|
|
|
696
1194
|
}
|
|
697
1195
|
};
|
|
698
1196
|
//#endregion
|
|
699
|
-
export {
|
|
1197
|
+
export { WriteGuard as S, tWorkspaceSessions as _, EVENT_ENCODING as a, postgresTableDefs as b, eventDimensions as c, tPersistenceState as d, tSchemaMeta as f, tStorageUnits as g, tSessions as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionProjcacheRows as m, openDatabase as n, SCHEMA_VERSION as o, tSessionEvents as p, createStorageRepository as r, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as s, SqliteBackend as t, tEvents as u, tWorkspaceState as v, toPostgresSchema as x, tWorkspaces as y };
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as EventRow, S as BackendTx, T as StorageRepository, _ as tWorkspaceSessions, a as JournalMode, b as WriteGuard, c as eventDimensions, d as tPersistenceState, f as tSchemaMeta, g as tStorageUnits, h as tSessions, i as EventRole, l as eventKind, m as tSessionProjcacheRows, n as EVENT_ENCODING, o as SCHEMA_VERSION, p as tSessionEvents, r as EventKind, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as DEFAULT_BUSY_TIMEOUT_MS, u as tEvents, v as tWorkspaceState, w as SessionRow, x as Backend, y as tWorkspaces } from "./schema-Bo6Hy5gW.mjs";
|
|
2
2
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
//#region src/sqlite.d.ts
|
|
@@ -12,10 +12,16 @@ declare class SqliteBackend implements Backend {
|
|
|
12
12
|
private readonly options;
|
|
13
13
|
readonly kind: "sqlite";
|
|
14
14
|
storeIdentity: string;
|
|
15
|
+
/** storages 接管表访问层;句柄在 open() 完成后解析。 */
|
|
16
|
+
readonly storage: StorageRepository;
|
|
15
17
|
private dbPath;
|
|
16
18
|
private db;
|
|
19
|
+
private readonly dbReady;
|
|
20
|
+
private resolveDb;
|
|
21
|
+
private rejectDb;
|
|
17
22
|
constructor(options: SqliteBackendOptions);
|
|
18
23
|
open(): Promise<void>;
|
|
24
|
+
private doOpen;
|
|
19
25
|
close(): Promise<void>;
|
|
20
26
|
getSession(id: SessionId): Promise<SessionRow | undefined>;
|
|
21
27
|
getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]>;
|
|
@@ -31,9 +37,11 @@ declare class SqliteBackend implements Backend {
|
|
|
31
37
|
private insertBridges;
|
|
32
38
|
private updateHead;
|
|
33
39
|
private bumpRevision;
|
|
40
|
+
/** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
|
|
41
|
+
private refreshTitle;
|
|
34
42
|
private deleteBridgeTail;
|
|
35
43
|
private getPrevBridge;
|
|
36
44
|
private eventRows;
|
|
37
45
|
}
|
|
38
46
|
//#endregion
|
|
39
|
-
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
|
|
47
|
+
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
|
package/dist/storage.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
|
|
1
|
+
import { S as WriteGuard, _ as tWorkspaceSessions, a as EVENT_ENCODING, c as eventDimensions, d as tPersistenceState, f as tSchemaMeta, g as tStorageUnits, h as tSessions, i as DEFAULT_BUSY_TIMEOUT_MS, l as eventKind, m as tSessionProjcacheRows, n as openDatabase, o as SCHEMA_VERSION, p as tSessionEvents, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as SqliteBackend, u as tEvents, v as tWorkspaceState, y as tWorkspaces } from "./sqlite-BIPdMNQb.mjs";
|
|
2
|
+
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SessionPersistence } from "@deepseek-ai/dsh-session-persistence";
|
|
2
2
|
import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
-
import { SettingsNamespace, SettingsProvider } from "@deepseek-ai/dsh-settings";
|
|
4
3
|
import { Context, Fiber } from "@deepseek-ai/cordis";
|
|
4
|
+
import { SettingsNamespace, SettingsProvider } from "@deepseek-ai/dsh-settings";
|
|
5
5
|
//#region src/testing/helpers.d.ts
|
|
6
6
|
declare class EmptySettings extends SettingsProvider {
|
|
7
7
|
constructor(ctx: Context);
|
package/dist/testing.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
|
|
2
2
|
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
3
4
|
import { SettingsProvider } from "@deepseek-ai/dsh-settings";
|
|
4
5
|
import { MessageId, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
6
6
|
//#region src/testing/helpers.ts
|
|
7
7
|
var EmptySettings = class extends SettingsProvider {
|
|
8
8
|
constructor(ctx) {
|