@morlay/session-rdb 0.0.19 → 0.0.21-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -31
- package/dist/artifact.d.mts +25 -65
- package/dist/artifact.mjs +3 -3
- package/dist/{schema-DPcuEh_a.d.mts → backend-DpdtxYpz.d.mts} +360 -107
- package/dist/branch-5JzX9rUq.mjs +390 -0
- package/dist/deletion.d.mts +7 -0
- package/dist/deletion.mjs +2 -0
- package/dist/dist-vIVO6bA-.mjs +1524 -0
- package/dist/import.d.mts +4 -4
- package/dist/import.mjs +214 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +4 -1630
- package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
- package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
- package/dist/src-CWTWV7vx.mjs +1904 -0
- package/dist/storage.d.mts +14 -5
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +2 -26
- package/dist/testing.mjs +10504 -9614
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
- package/package.json +29 -30
- package/src/adapters/to-postgres.ts +1 -3
- package/src/adapters/to-sqlite.ts +0 -2
- package/src/adapters/types.ts +0 -3
- package/src/artifact.ts +0 -2
- package/src/backend.ts +31 -2
- package/src/branch.ts +223 -135
- package/src/deletion.ts +87 -0
- package/src/drizzle/postgres-v2.ts +0 -1
- package/src/drizzle/postgres-v3.ts +0 -1
- package/src/drizzle/sqlite-v2.ts +0 -1
- package/src/drizzle/sqlite-v3.ts +0 -1
- package/src/entities/v2/session-events.ts +0 -1
- package/src/entities/v3/event-usage.ts +25 -0
- package/src/entities/v3/events.ts +1 -3
- package/src/entities/v3/index.ts +4 -0
- package/src/entities/v3/session-events.ts +1 -2
- package/src/entities/v3/session-projcache-rows.ts +0 -8
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-units.ts +0 -1
- package/src/entities/v3/workspace-sessions.ts +0 -5
- package/src/entities/v3/workspace-state.ts +0 -6
- package/src/entities/v3/workspaces.ts +0 -5
- package/src/export.ts +103 -0
- package/src/gc.ts +76 -0
- package/src/import-storages.ts +4 -37
- package/src/import.ts +54 -31
- package/src/index.ts +150 -114
- package/src/legacy.ts +4 -42
- package/src/log.ts +63 -106
- package/src/postgres.ts +249 -22
- package/src/schema.ts +6 -6
- package/src/session-query.ts +0 -4
- package/src/sqlite.ts +231 -55
- package/src/storage-takeover/index.ts +2 -28
- package/src/storage-takeover/projection-cache.ts +30 -134
- package/src/storage-takeover/repository.ts +20 -59
- package/src/storage-takeover/storage-backend.ts +3 -33
- package/src/storage-takeover/types.ts +14 -56
- package/src/storage.ts +0 -2
- package/src/testing/contract.ts +19 -50
- package/src/testing/coordinator-contract.ts +10 -25
- package/src/testing.ts +2 -2
- package/src/usage.ts +191 -0
- package/dist/import-Bc2QQa5G.mjs +0 -473
- package/dist/index-CCcK9tia.d.mts +0 -270
- package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow } from "./log-
|
|
1
|
+
import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, v as usageRowOf } from "./log-CnYct2Dv.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { and, desc, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
|
|
5
5
|
import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
|
|
6
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";
|
|
7
|
-
import {
|
|
8
|
-
import { mkdir, open } from "node:fs/promises";
|
|
7
|
+
import { mkdir, open, readdir, stat } from "node:fs/promises";
|
|
9
8
|
import { dirname, resolve } from "node:path";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
11
10
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -198,11 +197,8 @@ const sessions = {
|
|
|
198
197
|
type: "integer",
|
|
199
198
|
notNull: true
|
|
200
199
|
},
|
|
201
|
-
/** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
|
|
202
200
|
f_archived_at: { type: "bigint" },
|
|
203
|
-
/** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
|
|
204
201
|
f_title: { type: "text" },
|
|
205
|
-
/** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
|
|
206
202
|
f_title_seq: { type: "integer" }
|
|
207
203
|
}
|
|
208
204
|
};
|
|
@@ -310,7 +306,6 @@ const sessionEvents$1 = {
|
|
|
310
306
|
};
|
|
311
307
|
//#endregion
|
|
312
308
|
//#region src/entities/v3/storage-units.ts
|
|
313
|
-
/** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
|
|
314
309
|
const storageUnits = {
|
|
315
310
|
name: "t_storage_units",
|
|
316
311
|
columns: {
|
|
@@ -326,11 +321,6 @@ const storageUnits = {
|
|
|
326
321
|
};
|
|
327
322
|
//#endregion
|
|
328
323
|
//#region src/entities/v3/workspaces.ts
|
|
329
|
-
/**
|
|
330
|
-
* workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
|
|
331
|
-
* 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
|
|
332
|
-
* 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
|
|
333
|
-
*/
|
|
334
324
|
const workspaces = {
|
|
335
325
|
name: "t_workspaces",
|
|
336
326
|
columns: {
|
|
@@ -368,11 +358,6 @@ const workspaces = {
|
|
|
368
358
|
};
|
|
369
359
|
//#endregion
|
|
370
360
|
//#region src/entities/v3/workspace-sessions.ts
|
|
371
|
-
/**
|
|
372
|
-
* workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
|
|
373
|
-
* 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
|
|
374
|
-
* session 反查归属 workspace。
|
|
375
|
-
*/
|
|
376
361
|
const workspaceSessions = {
|
|
377
362
|
name: "t_workspace_sessions",
|
|
378
363
|
columns: {
|
|
@@ -403,12 +388,6 @@ const workspaceSessions = {
|
|
|
403
388
|
};
|
|
404
389
|
//#endregion
|
|
405
390
|
//#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_sessions.f_archived_at` 承载(归档不触碰归属槽位)。
|
|
411
|
-
*/
|
|
412
391
|
const workspaceState = {
|
|
413
392
|
name: "t_workspace_state",
|
|
414
393
|
columns: {
|
|
@@ -427,14 +406,6 @@ const workspaceState = {
|
|
|
427
406
|
};
|
|
428
407
|
//#endregion
|
|
429
408
|
//#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
409
|
const sessionProjcacheRows = {
|
|
439
410
|
name: "t_session_projcache_row",
|
|
440
411
|
columns: {
|
|
@@ -472,6 +443,53 @@ const sessionProjcacheRows = {
|
|
|
472
443
|
indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
|
|
473
444
|
};
|
|
474
445
|
//#endregion
|
|
446
|
+
//#region src/entities/v3/event-usage.ts
|
|
447
|
+
/**
|
|
448
|
+
* 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
|
|
449
|
+
* 统计只读这张表,不再逐行解析事件的 JSON;事件行本身仍留在 `t_events`。
|
|
450
|
+
*/
|
|
451
|
+
const eventUsage = {
|
|
452
|
+
name: "t_event_usage",
|
|
453
|
+
columns: {
|
|
454
|
+
f_event_id: {
|
|
455
|
+
type: "text",
|
|
456
|
+
primaryKey: true,
|
|
457
|
+
references: {
|
|
458
|
+
table: "t_events",
|
|
459
|
+
column: "f_event_id",
|
|
460
|
+
onDelete: "cascade"
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
f_created_at: {
|
|
464
|
+
type: "integer",
|
|
465
|
+
notNull: true
|
|
466
|
+
},
|
|
467
|
+
f_provider: { type: "text" },
|
|
468
|
+
f_model: { type: "text" },
|
|
469
|
+
f_input_tokens: {
|
|
470
|
+
type: "integer",
|
|
471
|
+
notNull: true
|
|
472
|
+
},
|
|
473
|
+
f_output_tokens: {
|
|
474
|
+
type: "integer",
|
|
475
|
+
notNull: true
|
|
476
|
+
},
|
|
477
|
+
f_cache_read_tokens: {
|
|
478
|
+
type: "integer",
|
|
479
|
+
notNull: true
|
|
480
|
+
},
|
|
481
|
+
f_reasoning_tokens: {
|
|
482
|
+
type: "integer",
|
|
483
|
+
notNull: true
|
|
484
|
+
},
|
|
485
|
+
f_total_tokens: {
|
|
486
|
+
type: "integer",
|
|
487
|
+
notNull: true
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
|
|
491
|
+
};
|
|
492
|
+
//#endregion
|
|
475
493
|
//#region src/entities/v3/index.ts
|
|
476
494
|
const sqliteTableDefs = [
|
|
477
495
|
persistenceState,
|
|
@@ -483,7 +501,8 @@ const sqliteTableDefs = [
|
|
|
483
501
|
workspaces,
|
|
484
502
|
workspaceSessions,
|
|
485
503
|
workspaceState,
|
|
486
|
-
sessionProjcacheRows
|
|
504
|
+
sessionProjcacheRows,
|
|
505
|
+
eventUsage
|
|
487
506
|
];
|
|
488
507
|
const postgresTableDefs = [
|
|
489
508
|
persistenceState,
|
|
@@ -495,7 +514,8 @@ const postgresTableDefs = [
|
|
|
495
514
|
workspaces,
|
|
496
515
|
workspaceSessions,
|
|
497
516
|
workspaceState,
|
|
498
|
-
sessionProjcacheRows
|
|
517
|
+
sessionProjcacheRows,
|
|
518
|
+
eventUsage
|
|
499
519
|
];
|
|
500
520
|
({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
|
|
501
521
|
//#endregion
|
|
@@ -514,6 +534,7 @@ const tWorkspaces = sqliteTables["t_workspaces"];
|
|
|
514
534
|
const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
|
|
515
535
|
const tWorkspaceState = sqliteTables["t_workspace_state"];
|
|
516
536
|
const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
|
|
537
|
+
const tEventUsage = sqliteTables["t_event_usage"];
|
|
517
538
|
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
518
539
|
function eventKind(event) {
|
|
519
540
|
switch (event.type) {
|
|
@@ -671,27 +692,16 @@ function eventDimensions(event) {
|
|
|
671
692
|
}
|
|
672
693
|
//#endregion
|
|
673
694
|
//#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
695
|
async function allRows(query) {
|
|
684
696
|
const q = query;
|
|
685
697
|
if (typeof q.all === "function") return q.all();
|
|
686
698
|
return await query;
|
|
687
699
|
}
|
|
688
|
-
/** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
|
|
689
700
|
async function oneRow(query) {
|
|
690
701
|
const q = query;
|
|
691
702
|
if (typeof q.get === "function") return q.get();
|
|
692
703
|
return (await query)[0];
|
|
693
704
|
}
|
|
694
|
-
/** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
|
|
695
705
|
async function runQuery(query) {
|
|
696
706
|
const q = query;
|
|
697
707
|
if (typeof q.run === "function") {
|
|
@@ -700,7 +710,6 @@ async function runQuery(query) {
|
|
|
700
710
|
}
|
|
701
711
|
await query;
|
|
702
712
|
}
|
|
703
|
-
/** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
|
|
704
713
|
function identityOfSession(row) {
|
|
705
714
|
return {
|
|
706
715
|
formatVersion: row.fVersion,
|
|
@@ -710,11 +719,9 @@ function identityOfSession(row) {
|
|
|
710
719
|
...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
|
|
711
720
|
};
|
|
712
721
|
}
|
|
713
|
-
/** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
|
|
714
722
|
function seqCursor(seq) {
|
|
715
723
|
return seq === -1 ? -1 : SessionSeq(seq);
|
|
716
724
|
}
|
|
717
|
-
/** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
|
|
718
725
|
function pendingMutationOf(row) {
|
|
719
726
|
const operation = row.fPendingOperation;
|
|
720
727
|
if (operation !== "create" && operation !== "delete") return void 0;
|
|
@@ -723,11 +730,6 @@ function pendingMutationOf(row) {
|
|
|
723
730
|
workspaceId: row.fPendingWorkspaceId ?? ""
|
|
724
731
|
};
|
|
725
732
|
}
|
|
726
|
-
/**
|
|
727
|
-
* 构建一个介质的 storages 接管访问层。
|
|
728
|
-
* @param host - 介质句柄、方言表对象与事务执行器。
|
|
729
|
-
* @returns 方言无关的访问层。
|
|
730
|
-
*/
|
|
731
733
|
function createStorageRepository(host) {
|
|
732
734
|
const tables = host.tables;
|
|
733
735
|
const tUnits = tables["t_storage_units"];
|
|
@@ -736,7 +738,6 @@ function createStorageRepository(host) {
|
|
|
736
738
|
const tWorkspaceSessions = tables["t_workspace_sessions"];
|
|
737
739
|
const tWorkspaceState = tables["t_workspace_state"];
|
|
738
740
|
const tProjcacheRows = tables["t_session_projcache_row"];
|
|
739
|
-
/** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
|
|
740
741
|
const sessionIdentity = {
|
|
741
742
|
fSessionId: tSessions.fSessionId,
|
|
742
743
|
fVersion: tSessions.fVersion,
|
|
@@ -905,9 +906,9 @@ function createStorageRepository(host) {
|
|
|
905
906
|
await runQuery(db.delete(tProjcacheRows).where(inArray(tProjcacheRows.fSessionId, stale.map((row) => row.fSessionId))));
|
|
906
907
|
return stale.length;
|
|
907
908
|
},
|
|
908
|
-
...host.
|
|
909
|
-
|
|
910
|
-
const db = host.
|
|
909
|
+
...host.dbDirect === void 0 ? {} : {
|
|
910
|
+
readProjcacheDirect: (sessionId) => {
|
|
911
|
+
const db = host.dbDirect();
|
|
911
912
|
const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
|
|
912
913
|
if (stored.length === 0) return void 0;
|
|
913
914
|
const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
@@ -924,8 +925,8 @@ function createStorageRepository(host) {
|
|
|
924
925
|
rows
|
|
925
926
|
};
|
|
926
927
|
},
|
|
927
|
-
|
|
928
|
-
const row = host.
|
|
928
|
+
readSessionTitleDirect: (sessionId) => {
|
|
929
|
+
const row = host.dbDirect().select({
|
|
929
930
|
fTitle: tSessions.fTitle,
|
|
930
931
|
fTitleSeq: tSessions.fTitleSeq
|
|
931
932
|
}).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
@@ -940,7 +941,45 @@ function createStorageRepository(host) {
|
|
|
940
941
|
}
|
|
941
942
|
//#endregion
|
|
942
943
|
//#region src/sqlite.ts
|
|
943
|
-
/**
|
|
944
|
+
/**
|
|
945
|
+
* 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
|
|
946
|
+
*/
|
|
947
|
+
function backfillEventUsage(db) {
|
|
948
|
+
if (db.prepare("SELECT count(*) AS n FROM t_event_usage").get().n > 0) return;
|
|
949
|
+
db.exec(`INSERT OR IGNORE INTO t_event_usage (
|
|
950
|
+
f_event_id, f_created_at, f_provider, f_model,
|
|
951
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
952
|
+
SELECT e.f_event_id, e.f_created_at,
|
|
953
|
+
coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
|
|
954
|
+
json_extract(e.f_data, '$.message.source.provider')),
|
|
955
|
+
coalesce(json_extract(e.f_data, '$.data.message.source.model'),
|
|
956
|
+
json_extract(e.f_data, '$.message.source.model')),
|
|
957
|
+
coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
|
|
958
|
+
json_extract(e.f_data, '$.usage.inputTokens'), 0),
|
|
959
|
+
coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
|
|
960
|
+
json_extract(e.f_data, '$.usage.outputTokens'), 0),
|
|
961
|
+
coalesce(json_extract(e.f_data, '$.data.usage.cacheReadTokens'),
|
|
962
|
+
json_extract(e.f_data, '$.usage.cacheReadTokens'), 0),
|
|
963
|
+
coalesce(json_extract(e.f_data, '$.data.usage.reasoningTokens'),
|
|
964
|
+
json_extract(e.f_data, '$.usage.reasoningTokens'), 0),
|
|
965
|
+
coalesce(json_extract(e.f_data, '$.data.usage.totalTokens'),
|
|
966
|
+
json_extract(e.f_data, '$.usage.totalTokens'), 0)
|
|
967
|
+
FROM t_events e
|
|
968
|
+
WHERE e.f_type = 'assistant/message'
|
|
969
|
+
AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
|
|
970
|
+
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
971
|
+
}
|
|
972
|
+
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
973
|
+
function totalsOf(row) {
|
|
974
|
+
return {
|
|
975
|
+
events: row.events,
|
|
976
|
+
inputTokens: row.input_tokens ?? 0,
|
|
977
|
+
outputTokens: row.output_tokens ?? 0,
|
|
978
|
+
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
979
|
+
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
980
|
+
totalTokens: row.total_tokens ?? 0
|
|
981
|
+
};
|
|
982
|
+
}
|
|
944
983
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
945
984
|
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
946
985
|
function enqueueSqliteTx(path, fn) {
|
|
@@ -955,14 +994,15 @@ async function createDatabaseFile(path) {
|
|
|
955
994
|
if (error.code !== "EEXIST") throw error;
|
|
956
995
|
}
|
|
957
996
|
}
|
|
958
|
-
function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
|
|
997
|
+
async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
|
|
959
998
|
const db = new DatabaseSync(path);
|
|
960
999
|
try {
|
|
961
1000
|
configureDatabase(db, path, journalMode, busyTimeout);
|
|
962
1001
|
const dbx = drizzle({ client: db });
|
|
963
1002
|
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
|
|
964
|
-
if (onDisk === 2 && !hasMigrationsTable(db)) baselineV2(db);
|
|
1003
|
+
if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
|
|
965
1004
|
migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
1005
|
+
backfillEventUsage(db);
|
|
966
1006
|
dbx.insert(tPersistenceState).values({
|
|
967
1007
|
fSingleton: 1,
|
|
968
1008
|
fStoreId: randomUUID()
|
|
@@ -978,9 +1018,8 @@ function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS)
|
|
|
978
1018
|
function hasMigrationsTable(db) {
|
|
979
1019
|
return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'").get() !== void 0;
|
|
980
1020
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const dir = readdirSync(sqliteMigrationsDir).find((name) => name.endsWith("_v2_initial"));
|
|
1021
|
+
async function baselineV2(db) {
|
|
1022
|
+
const dir = (await readdir(sqliteMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
|
|
984
1023
|
if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/sqlite");
|
|
985
1024
|
db.exec(`
|
|
986
1025
|
CREATE TABLE __drizzle_migrations (
|
|
@@ -1013,7 +1052,6 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1013
1052
|
options;
|
|
1014
1053
|
kind = "sqlite";
|
|
1015
1054
|
storeIdentity;
|
|
1016
|
-
/** storages 接管表访问层;句柄在 open() 完成后解析。 */
|
|
1017
1055
|
storage;
|
|
1018
1056
|
dbPath = "";
|
|
1019
1057
|
db;
|
|
@@ -1029,7 +1067,7 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1029
1067
|
this.dbReady.catch(() => {});
|
|
1030
1068
|
this.storage = createStorageRepository({
|
|
1031
1069
|
db: () => this.dbReady,
|
|
1032
|
-
|
|
1070
|
+
dbDirect: () => {
|
|
1033
1071
|
if (this.db === void 0) throw new Error("sqlite session database is not open");
|
|
1034
1072
|
return this.db;
|
|
1035
1073
|
},
|
|
@@ -1077,16 +1115,15 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1077
1115
|
await createDatabaseFile(actual);
|
|
1078
1116
|
}
|
|
1079
1117
|
await enqueueSqliteTx(actual, async () => {
|
|
1080
|
-
this.db = drizzle({ client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
|
|
1118
|
+
this.db = drizzle({ client: await openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
|
|
1081
1119
|
migrate(this.db, { migrationsFolder: sqliteMigrationsDir });
|
|
1082
1120
|
});
|
|
1083
1121
|
try {
|
|
1084
1122
|
const row = this.db.select({ fStoreId: tPersistenceState.fStoreId }).from(tPersistenceState).where(eq(tPersistenceState.fSingleton, 1)).get();
|
|
1085
|
-
/* v8 ignore next -- openDatabase inserts the singleton before returning. */
|
|
1086
1123
|
if (row === void 0) throw new Error(`session database at "${actual}" has no store identity`);
|
|
1087
1124
|
if (row.fStoreId.length === 0) throw new Error(`session database at "${actual}" has no valid store identity`);
|
|
1088
1125
|
if (actual !== ":memory:") {
|
|
1089
|
-
const identity =
|
|
1126
|
+
const identity = await stat(actual, { bigint: true });
|
|
1090
1127
|
this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
|
|
1091
1128
|
} else this.storeIdentity = `memory:store:${row.fStoreId}`;
|
|
1092
1129
|
} catch (error) {
|
|
@@ -1104,6 +1141,12 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1104
1141
|
async getEventRows(id, fromSequence) {
|
|
1105
1142
|
return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
|
|
1106
1143
|
}
|
|
1144
|
+
async getEventTypeAt(id, sequence) {
|
|
1145
|
+
return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get()?.fType;
|
|
1146
|
+
}
|
|
1147
|
+
async getEventTypesBefore(id, beforeSequence, limit) {
|
|
1148
|
+
return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence))).orderBy(desc(tSessionEvents.fSequence)).limit(limit).all();
|
|
1149
|
+
}
|
|
1107
1150
|
async listSessions() {
|
|
1108
1151
|
return this.db.select().from(tSessions).all();
|
|
1109
1152
|
}
|
|
@@ -1115,7 +1158,6 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1115
1158
|
this.db.$client.exec("COMMIT");
|
|
1116
1159
|
return result;
|
|
1117
1160
|
} catch (error) {
|
|
1118
|
-
/* v8 ignore start */
|
|
1119
1161
|
try {
|
|
1120
1162
|
this.db.$client.exec("ROLLBACK");
|
|
1121
1163
|
} catch {}
|
|
@@ -1134,7 +1176,9 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1134
1176
|
bumpRevision: (id) => this.bumpRevision(id),
|
|
1135
1177
|
refreshTitle: (id) => this.refreshTitle(id),
|
|
1136
1178
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
1137
|
-
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
|
|
1179
|
+
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
|
|
1180
|
+
deleteSession: (id) => this.deleteSession(id),
|
|
1181
|
+
deleteSessions: (ids) => this.deleteSessions(ids)
|
|
1138
1182
|
};
|
|
1139
1183
|
async upsertSession(storage, incarnation) {
|
|
1140
1184
|
this.db.insert(tSessions).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
|
|
@@ -1147,13 +1191,11 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1147
1191
|
fHeadEventId: tSessions.fHeadEventId,
|
|
1148
1192
|
fHeadSequence: tSessions.fHeadSequence
|
|
1149
1193
|
}).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
|
|
1150
|
-
/* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
|
|
1151
1194
|
if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1152
1195
|
return head;
|
|
1153
1196
|
}
|
|
1154
1197
|
async getSeedLength(id) {
|
|
1155
1198
|
const row = this.db.select({ fSeedLength: tSessions.fSeedLength }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
|
|
1156
|
-
/* v8 ignore next -- rewind always materializes the row before reading the seed length */
|
|
1157
1199
|
if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
|
|
1158
1200
|
return row.fSeedLength;
|
|
1159
1201
|
}
|
|
@@ -1164,6 +1206,11 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1164
1206
|
async insertEvents(events) {
|
|
1165
1207
|
if (events.length === 0) return;
|
|
1166
1208
|
for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tEvents).values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).run();
|
|
1209
|
+
const usage = events.flatMap((event) => {
|
|
1210
|
+
const row = usageRowOf(event);
|
|
1211
|
+
return row === void 0 ? [] : [row];
|
|
1212
|
+
});
|
|
1213
|
+
if (usage.length > 0) this.db.insert(tEventUsage).values(usage).onConflictDoNothing().run();
|
|
1167
1214
|
}
|
|
1168
1215
|
async insertBridges(rows) {
|
|
1169
1216
|
if (rows.length === 0) return;
|
|
@@ -1178,7 +1225,6 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1178
1225
|
async bumpRevision(id) {
|
|
1179
1226
|
this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
|
|
1180
1227
|
}
|
|
1181
|
-
/** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
|
|
1182
1228
|
async refreshTitle(id) {
|
|
1183
1229
|
const row = this.db.select({
|
|
1184
1230
|
fSequence: tSessionEvents.fSequence,
|
|
@@ -1199,6 +1245,90 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1199
1245
|
fSequence: tSessionEvents.fSequence
|
|
1200
1246
|
}).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get();
|
|
1201
1247
|
}
|
|
1248
|
+
async deleteSession(id) {
|
|
1249
|
+
this.db.delete(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).run();
|
|
1250
|
+
this.db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fSessionId, id)).run();
|
|
1251
|
+
this.db.delete(tSessionProjcacheRows).where(eq(tSessionProjcacheRows.fSessionId, id)).run();
|
|
1252
|
+
this.db.delete(tSessions).where(eq(tSessions.fSessionId, id)).run();
|
|
1253
|
+
}
|
|
1254
|
+
/** 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。 */
|
|
1255
|
+
async collectOrphans() {
|
|
1256
|
+
const referenced = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents);
|
|
1257
|
+
const info = this.db.delete(tEvents).where(notInArray(tEvents.fEventId, referenced)).run();
|
|
1258
|
+
this.db.delete(tEventUsage).where(notInArray(tEventUsage.fEventId, this.db.select({ fEventId: tEvents.fEventId }).from(tEvents))).run();
|
|
1259
|
+
return Number(info.changes);
|
|
1260
|
+
}
|
|
1261
|
+
/** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
|
|
1262
|
+
async listOrphanSubagentSessions() {
|
|
1263
|
+
return this.db.$client.prepare(`SELECT f_session_id AS id FROM t_sessions
|
|
1264
|
+
WHERE f_origin = 'subagent'
|
|
1265
|
+
AND (f_parent_session IS NULL
|
|
1266
|
+
OR f_parent_session NOT IN (SELECT f_session_id FROM t_sessions))`).all().map((row) => row.id);
|
|
1267
|
+
}
|
|
1268
|
+
async deleteSessions(ids) {
|
|
1269
|
+
if (ids.length === 0) return 0;
|
|
1270
|
+
this.db.delete(tSessionEvents).where(inArray(tSessionEvents.fSessionId, ids)).run();
|
|
1271
|
+
this.db.delete(tWorkspaceSessions).where(inArray(tWorkspaceSessions.fSessionId, ids)).run();
|
|
1272
|
+
this.db.delete(tSessionProjcacheRows).where(inArray(tSessionProjcacheRows.fSessionId, ids)).run();
|
|
1273
|
+
const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
|
|
1274
|
+
return Number(info.changes);
|
|
1275
|
+
}
|
|
1276
|
+
/** 用量聚合:只读 `t_event_usage`;可选只算 `sinceMs` 之后的事件行。 */
|
|
1277
|
+
async usageReport(sinceMs) {
|
|
1278
|
+
const sinceClause = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
|
|
1279
|
+
const sinceParams = sinceMs === void 0 ? [] : [sinceMs];
|
|
1280
|
+
const buckets = this.db.$client.prepare(`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
|
|
1281
|
+
u.f_provider AS provider,
|
|
1282
|
+
u.f_model AS model,
|
|
1283
|
+
EXISTS (SELECT 1 FROM t_session_events sb
|
|
1284
|
+
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
1285
|
+
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
|
|
1286
|
+
count(*) AS events,
|
|
1287
|
+
sum(u.f_input_tokens) AS input_tokens,
|
|
1288
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
1289
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
1290
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
1291
|
+
sum(u.f_total_tokens) AS total_tokens
|
|
1292
|
+
FROM t_event_usage u
|
|
1293
|
+
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
|
|
1294
|
+
GROUP BY day, provider, model, subagent`).all(...sinceParams);
|
|
1295
|
+
const sessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
|
|
1296
|
+
s.f_title AS title,
|
|
1297
|
+
(s.f_origin = 'subagent') AS subagent,
|
|
1298
|
+
(s.f_archived_at IS NOT NULL) AS archived,
|
|
1299
|
+
count(*) AS events,
|
|
1300
|
+
sum(u.f_input_tokens) AS input_tokens,
|
|
1301
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
1302
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
1303
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
1304
|
+
sum(u.f_total_tokens) AS total_tokens
|
|
1305
|
+
FROM t_session_events b
|
|
1306
|
+
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
1307
|
+
JOIN t_sessions s ON s.f_session_id = b.f_session_id
|
|
1308
|
+
WHERE 1 = 1${sinceClause}
|
|
1309
|
+
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...sinceParams);
|
|
1310
|
+
return {
|
|
1311
|
+
buckets: buckets.map((row) => ({
|
|
1312
|
+
day: row.day,
|
|
1313
|
+
provider: row.provider,
|
|
1314
|
+
model: row.model,
|
|
1315
|
+
subagent: row.subagent === 1,
|
|
1316
|
+
...totalsOf(row)
|
|
1317
|
+
})),
|
|
1318
|
+
sessions: sessions.map((row) => ({
|
|
1319
|
+
sessionId: row.session_id,
|
|
1320
|
+
title: row.title,
|
|
1321
|
+
subagent: row.subagent === 1,
|
|
1322
|
+
archived: row.archived === 1,
|
|
1323
|
+
...totalsOf(row)
|
|
1324
|
+
}))
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
async vacuum() {
|
|
1328
|
+
await enqueueSqliteTx(this.dbPath, async () => {
|
|
1329
|
+
this.db.$client.exec("VACUUM");
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1202
1332
|
eventRows() {
|
|
1203
1333
|
return this.db.select({
|
|
1204
1334
|
fEventId: tSessionEvents.fEventId,
|
|
@@ -1215,4 +1345,4 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1215
1345
|
}
|
|
1216
1346
|
};
|
|
1217
1347
|
//#endregion
|
|
1218
|
-
export { WriteGuard as S,
|
|
1348
|
+
export { WriteGuard as C, toPostgresSchema as S, tStorageUnits as _, EVENT_ENCODING as a, tWorkspaces as b, eventDimensions as c, tEvents as d, tPersistenceState as f, tSessions as g, tSessionProjcacheRows as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionEvents as m, openDatabase as n, SCHEMA_VERSION as o, tSchemaMeta as p, createStorageRepository as r, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as s, SqliteBackend as t, tEventUsage as u, tWorkspaceSessions as v, postgresTableDefs as x, tWorkspaceState as y };
|