@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +18 -3
  2. package/dist/artifact.d.mts +118 -2
  3. package/dist/artifact.mjs +5 -3
  4. package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
  5. package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
  6. package/dist/branch-CG1PUlO6.mjs +833 -0
  7. package/dist/deletion-SSX7IqU8.mjs +35 -0
  8. package/dist/deletion.d.mts +1 -1
  9. package/dist/deletion.mjs +2 -2
  10. package/dist/export-CfJQjQfr.mjs +123 -0
  11. package/dist/export.d.mts +1 -1
  12. package/dist/export.mjs +31 -55
  13. package/dist/import.d.mts +1 -1
  14. package/dist/import.mjs +31 -76
  15. package/dist/index-CUD2ZpTA.d.mts +71 -0
  16. package/dist/index.d.mts +20 -2
  17. package/dist/index.mjs +1561 -4
  18. package/dist/open-YoyD_kah.mjs +561 -0
  19. package/dist/organize.d.mts +9 -0
  20. package/dist/organize.mjs +30 -0
  21. package/dist/route-DpfY0zq-.mjs +114 -0
  22. package/dist/storage.d.mts +41 -48
  23. package/dist/storage.mjs +3 -1
  24. package/dist/testing.d.mts +9 -1
  25. package/dist/testing.mjs +25 -4
  26. package/dist/usage-row-BQQM-_t6.mjs +36 -0
  27. package/dist/usage.d.mts +1 -1
  28. package/dist/usage.mjs +7 -40
  29. package/dist/write-guard-CYrY58GM.mjs +174 -0
  30. package/package.json +32 -36
  31. package/src/artifact.ts +4 -1
  32. package/src/backend-rdb.ts +331 -0
  33. package/src/backend.ts +32 -15
  34. package/src/{branch.ts → branch/index.ts} +109 -110
  35. package/src/branch/port.ts +62 -0
  36. package/src/branch/semantics.ts +69 -0
  37. package/src/db/open.ts +180 -0
  38. package/src/db/stats.ts +449 -0
  39. package/src/deletion-error.ts +17 -0
  40. package/src/deletion.ts +19 -72
  41. package/src/entities/index.ts +16 -1
  42. package/src/entities/v3/event-usage.ts +3 -3
  43. package/src/entities/v3/events.ts +3 -3
  44. package/src/entities/v3/index.ts +4 -16
  45. package/src/entities/v3/persistence-state.ts +3 -3
  46. package/src/entities/v3/schema-meta.ts +3 -3
  47. package/src/entities/v3/session-counts.ts +3 -3
  48. package/src/entities/v3/session-events.ts +3 -3
  49. package/src/entities/v3/session-projcache-rows.ts +3 -3
  50. package/src/entities/v3/session-usage.ts +3 -3
  51. package/src/entities/v3/sessions.ts +3 -3
  52. package/src/entities/v3/storage-records.ts +17 -0
  53. package/src/entities/v3/storage-units.ts +3 -3
  54. package/src/{schema.ts → event-shape.ts} +0 -46
  55. package/src/export.ts +32 -78
  56. package/src/handles.ts +349 -0
  57. package/src/http/route.ts +178 -0
  58. package/src/import-storages.ts +5 -2
  59. package/src/import.ts +63 -125
  60. package/src/index.ts +15 -1666
  61. package/src/legacy.ts +3 -1
  62. package/src/live-routing.ts +205 -0
  63. package/src/log/export.ts +84 -0
  64. package/src/{log.ts → log/repair.ts} +1 -268
  65. package/src/log/rows.ts +107 -0
  66. package/src/log/usage-row.ts +64 -0
  67. package/src/migrations.ts +196 -0
  68. package/src/organize.ts +38 -0
  69. package/src/read-path.ts +190 -0
  70. package/src/routes.ts +2 -1
  71. package/src/rows.ts +25 -77
  72. package/src/session-ops.ts +444 -0
  73. package/src/session-store.ts +675 -0
  74. package/src/stats-fold.ts +44 -0
  75. package/src/storage-takeover/index.ts +2 -5
  76. package/src/storage-takeover/legacy-domain-migration.ts +118 -0
  77. package/src/storage-takeover/projection-cache.ts +122 -71
  78. package/src/storage-takeover/read-model.ts +189 -0
  79. package/src/storage-takeover/repository.ts +236 -442
  80. package/src/storage-takeover/storage-backend.ts +44 -159
  81. package/src/storage-takeover/types.ts +45 -19
  82. package/src/storage-takeover/workspace-documents.ts +65 -0
  83. package/src/storage.ts +4 -2
  84. package/src/testing.ts +21 -1
  85. package/src/usage.ts +11 -65
  86. package/src/version.ts +13 -0
  87. package/src/write-path.ts +286 -0
  88. package/dist/branch-CoPCRKer.mjs +0 -401
  89. package/dist/deletion-CWl89TIR.mjs +0 -2249
  90. package/dist/gc.d.mts +0 -4
  91. package/dist/gc.mjs +0 -48
  92. package/dist/log-CcWy46VE.mjs +0 -558
  93. package/dist/routes-hZjTcX_R.mjs +0 -9
  94. package/dist/sqlite-BNRd1aqR.mjs +0 -1778
  95. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
  96. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
  97. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
  98. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
  99. package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
  100. package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
  101. package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
  102. package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
  103. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
  104. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
  105. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
  106. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
  107. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
  108. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
  109. package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
  110. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
  111. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
  112. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
  113. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
  114. package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
  115. package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
  116. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
  117. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
  118. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
  119. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
  120. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
  121. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
  122. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
  123. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
  124. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
  125. package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
  126. package/src/adapters/index.ts +0 -12
  127. package/src/adapters/to-postgres.ts +0 -93
  128. package/src/adapters/to-sqlite.ts +0 -83
  129. package/src/adapters/types.ts +0 -44
  130. package/src/drizzle/postgres-v2.ts +0 -10
  131. package/src/drizzle/postgres-v3.ts +0 -18
  132. package/src/drizzle/sqlite-v2.ts +0 -9
  133. package/src/drizzle/sqlite-v3.ts +0 -18
  134. package/src/entities/v2/index.ts +0 -20
  135. package/src/entities/v2/session-events.ts +0 -10
  136. package/src/entities/v3/schedule-tasks.ts +0 -16
  137. package/src/entities/v3/workspace-sessions.ts +0 -19
  138. package/src/entities/v3/workspace-state.ts +0 -12
  139. package/src/entities/v3/workspaces.ts +0 -14
  140. package/src/gc.ts +0 -77
  141. package/src/postgres.ts +0 -1015
  142. package/src/sqlite.ts +0 -1096
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/session-rdb",
3
- "version": "0.1.10-alpha.0",
3
+ "version": "0.1.10-alpha.3",
4
4
  "description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
5
5
  "keywords": [
6
6
  "branch",
@@ -20,7 +20,6 @@
20
20
  "locale/*.json",
21
21
  "dist",
22
22
  "src",
23
- "drizzle",
24
23
  "!**/__tests__"
25
24
  ],
26
25
  "type": "module",
@@ -41,9 +40,9 @@
41
40
  "types": "./dist/export.d.mts",
42
41
  "default": "./dist/export.mjs"
43
42
  },
44
- "./gc": {
45
- "types": "./dist/gc.d.mts",
46
- "default": "./dist/gc.mjs"
43
+ "./organize": {
44
+ "types": "./dist/organize.d.mts",
45
+ "default": "./dist/organize.mjs"
47
46
  },
48
47
  "./import": {
49
48
  "types": "./dist/import.d.mts",
@@ -65,44 +64,41 @@
65
64
  "./locale/*.json": "./locale/*.json"
66
65
  },
67
66
  "dependencies": {
68
- "@morlay/session-branch": "0.1.10-alpha.0",
69
- "drizzle-orm": "^1.0.0-rc.5-ab785fc",
70
- "fflate": "^0.8.2",
71
- "pg": "^8.23.0"
67
+ "@morlay/rdbkit": "0.0.1-alpha.3",
68
+ "@morlay/session-branch": "0.1.10-alpha.3",
69
+ "fflate": "^0.8.2"
72
70
  },
73
71
  "devDependencies": {
74
- "@deepseek-ai/cosmokit": "^1.8.7-alpha.1",
75
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.2-alpha.2",
76
- "@deepseek-ai/dsh-compaction": "^0.2.2-alpha.2",
77
- "@deepseek-ai/dsh-session-turn-outline": "^0.2.2-alpha.2",
78
- "@deepseek-ai/dsh-storage": "^0.2.2-alpha.2",
79
- "@deepseek-ai/dsh-storage-domain": "^0.2.2-alpha.2",
80
- "@deepseek-ai/dsh-token-meter": "^0.2.2-alpha.2",
81
- "@types/pg": "^8.23.1",
82
- "drizzle-kit": "1.0.0-rc.5-ab785fc"
72
+ "@deepseek-ai/cosmokit": "^1.8.7-alpha.2",
73
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.2-alpha.3",
74
+ "@deepseek-ai/dsh-compaction": "^0.2.2-alpha.3",
75
+ "@deepseek-ai/dsh-cua": "^0.2.2-alpha.3",
76
+ "@deepseek-ai/dsh-session-turn-outline": "^0.2.2-alpha.3",
77
+ "@deepseek-ai/dsh-storage": "^0.2.2-alpha.3",
78
+ "@deepseek-ai/dsh-storage-domain": "^0.2.2-alpha.3",
79
+ "@deepseek-ai/dsh-token-meter": "^0.2.2-alpha.3"
83
80
  },
84
81
  "peerDependencies": {
85
- "@deepseek-ai/cordis": "^4.0.6-alpha.1",
86
- "@deepseek-ai/cosmokit": "^1.8.7-alpha.1",
87
- "@deepseek-ai/dsh-llm": "^0.2.2-alpha.2",
88
- "@deepseek-ai/dsh-schedule": "^0.2.2-alpha.2",
89
- "@deepseek-ai/dsh-scope": "^0.2.2-alpha.2",
90
- "@deepseek-ai/dsh-session": "^0.2.2-alpha.2",
91
- "@deepseek-ai/dsh-session-format": "^0.2.2-alpha.2",
92
- "@deepseek-ai/dsh-session-format-catalog": "^0.2.2-alpha.2",
93
- "@deepseek-ai/dsh-session-persistence": "^0.2.2-alpha.2",
94
- "@deepseek-ai/dsh-session-projection": "^0.2.2-alpha.2",
95
- "@deepseek-ai/dsh-session-projection-cache": "^0.2.2-alpha.2",
96
- "@deepseek-ai/dsh-session-query": "^0.2.2-alpha.2",
97
- "@deepseek-ai/dsh-storage": "^0.2.2-alpha.2",
98
- "@deepseek-ai/dsh-workspace": "^0.2.2-alpha.2",
99
- "@deepseek-ai/schemastery": "^3.18.6-alpha.1"
82
+ "@deepseek-ai/cordis": "^4.0.6-alpha.2",
83
+ "@deepseek-ai/cosmokit": "^1.8.7-alpha.2",
84
+ "@deepseek-ai/dsh-llm": "^0.2.2-alpha.3",
85
+ "@deepseek-ai/dsh-schedule": "^0.2.2-alpha.3",
86
+ "@deepseek-ai/dsh-scope": "^0.2.2-alpha.3",
87
+ "@deepseek-ai/dsh-session": "^0.2.2-alpha.3",
88
+ "@deepseek-ai/dsh-session-format": "^0.2.2-alpha.3",
89
+ "@deepseek-ai/dsh-session-format-catalog": "^0.2.2-alpha.3",
90
+ "@deepseek-ai/dsh-session-persistence": "^0.2.2-alpha.3",
91
+ "@deepseek-ai/dsh-session-projection": "^0.2.2-alpha.3",
92
+ "@deepseek-ai/dsh-session-projection-cache": "^0.2.2-alpha.3",
93
+ "@deepseek-ai/dsh-session-query": "^0.2.2-alpha.3",
94
+ "@deepseek-ai/dsh-storage": "^0.2.2-alpha.3",
95
+ "@deepseek-ai/dsh-workspace": "^0.2.2-alpha.3",
96
+ "@deepseek-ai/schemastery": "^3.18.6-alpha.2"
100
97
  },
101
98
  "engines": {
102
- "dsh": ">= 0.2.2-alpha.2 < 0.2.3"
99
+ "dsh": ">= 0.2.2-alpha.3 < 0.2.3"
103
100
  },
104
101
  "scripts": {
105
- "build": "pnpm exec tsdown",
106
- "db:generate": "drizzle-kit generate --config drizzle.config.ts && drizzle-kit generate --config drizzle.pg.config.ts"
102
+ "build": "pnpm exec tsdown"
107
103
  }
108
104
  }
package/src/artifact.ts CHANGED
@@ -1,2 +1,5 @@
1
- export * from "./log.ts";
1
+ export * from "./log/rows.ts";
2
+ export * from "./log/repair.ts";
3
+ export * from "./log/export.ts";
4
+ export * from "./log/usage-row.ts";
2
5
  export * from "./import.ts";
@@ -0,0 +1,331 @@
1
+ import { E, count, type Database } from "@morlay/rdbkit";
2
+ import type {
3
+ Backend,
4
+ BackendTx,
5
+ EventInsert,
6
+ SessionCountBucket,
7
+ SessionListRowsPage,
8
+ SessionListRowsQuery,
9
+ SessionRow,
10
+ SessionUsageBucket,
11
+ } from "./backend.ts";
12
+ import type { SessionId } from "@deepseek-ai/dsh-session";
13
+ import type { UsageAggregate } from "./usage.ts";
14
+ import {
15
+ eventUsage,
16
+ events,
17
+ sessionEvents,
18
+ sessions,
19
+ } from "./entities/index.ts";
20
+ import type { EventUsageRow } from "./log/usage-row.ts";
21
+ import { sessionHeaderColumns, sessionInsertColumns } from "./log/rows.ts";
22
+ import { titleOfEventData } from "./log/export.ts";
23
+ import { collectOrphans, readHead, readPrevBridge, readSeedLength } from "./session-ops.ts";
24
+ import {
25
+ headerChangedCondition,
26
+ lastEventAtExpr,
27
+ type ReadModel,
28
+ } from "./storage-takeover/read-model.ts";
29
+ import type { StorageRepository } from "./storage-takeover/types.ts";
30
+ import * as stats from "./db/stats.ts";
31
+ import { pruneWorkspaceDocuments } from "./storage-takeover/workspace-documents.ts";
32
+
33
+ const INSERT_BATCH_ROWS = 1000;
34
+
35
+ /**
36
+ * 唯一的 backend 实现:读写方法收敛在这里(读模型 + `session-ops` + 表句柄);打开流程在 `db/open.ts`,
37
+ * 统计面在 `db/stats.ts`。方言差异全部由 rdbkit 的表达式承担,这里不出现任何 SQL 文本。
38
+ */
39
+ export class RdbBackend implements Backend {
40
+ readonly storage: StorageRepository;
41
+ readonly storeIdentity: string;
42
+
43
+ readonly #database: Database;
44
+ readonly reads: ReadModel;
45
+
46
+ constructor(options: {
47
+ kind: "sqlite" | "postgres";
48
+ database: Database;
49
+ storage: StorageRepository;
50
+ readModel: ReadModel;
51
+ storeIdentity: string;
52
+ }) {
53
+ this.kind = options.kind;
54
+ this.#database = options.database;
55
+ this.storage = options.storage;
56
+ this.reads = options.readModel;
57
+ this.storeIdentity = options.storeIdentity;
58
+ }
59
+
60
+ readonly kind: "sqlite" | "postgres";
61
+
62
+ async open(): Promise<void> {
63
+ // 打开在 `openRdbBackend` 里完成(构造后即可用)。
64
+ }
65
+
66
+ async close(): Promise<void> {
67
+ await this.#database.close();
68
+ }
69
+
70
+ async transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T> {
71
+ return await this.#database.transaction(async (scoped) => fn(this.#txFor(scoped)));
72
+ }
73
+
74
+ async listOrphanSubagentSessions(): Promise<SessionId[]> {
75
+ return (await this.reads.listOrphanSubagentSessions()) as SessionId[];
76
+ }
77
+
78
+ /** 当前 head 指针(分支 / 测试读路径用;写路径在 `BackendTx` 里)。 */
79
+ async getHead(id: SessionId): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
80
+ return await readHead(this.#database, id);
81
+ }
82
+
83
+ async collectOrphans(): Promise<number> {
84
+ return await collectOrphans(this.#database);
85
+ }
86
+
87
+ async usageReport(sinceMs?: number): Promise<UsageAggregate> {
88
+ return await stats.usageReport(this.#database, sinceMs);
89
+ }
90
+
91
+ async listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage> {
92
+ return await stats.listSessionRows(this.#database, this.storage, query);
93
+ }
94
+
95
+ async incrementSessionUsage(id: string, buckets: readonly SessionUsageBucket[]): Promise<void> {
96
+ await stats.incrementSessionUsage(this.#database, id, buckets);
97
+ }
98
+
99
+ async incrementSessionCounts(id: string, buckets: readonly SessionCountBucket[]): Promise<void> {
100
+ await stats.incrementSessionCounts(this.#database, id, buckets);
101
+ }
102
+
103
+ async deleteSessionStats(id: string): Promise<void> {
104
+ await stats.deleteSessionStats(this.#database, id);
105
+ }
106
+
107
+ async rebuildSessionStats(id: string): Promise<void> {
108
+ await stats.rebuildSessionStats(this.#database, id);
109
+ }
110
+
111
+ async rebuildAllStats(): Promise<number> {
112
+ return await stats.rebuildAllStats(this.#database);
113
+ }
114
+
115
+ async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
116
+ await stats.refreshEventUsageFlags(this.#database, eventIds);
117
+ }
118
+
119
+ async vacuum(): Promise<void> {
120
+ await this.#database.vacuum();
121
+ }
122
+
123
+ #txFor(rdb: Database): BackendTx {
124
+ return {
125
+ upsertSession: (storage, incarnation) => this.#upsertSession(rdb, storage, incarnation),
126
+ getHead: (id) => readHead(rdb, id),
127
+ getSeedLength: (id) => readSeedLength(rdb, id),
128
+ updateSeedLength: (id, seedLength) => this.#updateSeedLength(rdb, id, seedLength),
129
+ refreshTitle: (id) => this.#refreshTitle(rdb, id),
130
+ insertEvents: (events, usageRows) => this.#insertEvents(rdb, events, usageRows),
131
+ insertBridges: (rows) => this.#insertBridges(rdb, rows),
132
+ updateHead: (id, headEventId, headSequence) =>
133
+ this.#updateHead(rdb, id, headEventId, headSequence),
134
+ updateHeadAndRevision: (id, headEventId, headSequence, lastEventAt) =>
135
+ this.#updateHeadAndRevision(rdb, id, headEventId, headSequence, lastEventAt),
136
+ bumpRevision: (id, lastEventAt) => this.#bumpRevision(rdb, id, lastEventAt),
137
+ deleteBridgeTail: (id, fromSequence) => this.#deleteBridgeTail(rdb, id, fromSequence),
138
+ getPrevBridge: (id, sequence) => readPrevBridge(rdb, id, sequence),
139
+ deleteSession: (id) => this.#deleteSession(rdb, id),
140
+ deleteSessions: (ids) => this.#deleteSessions(rdb, ids),
141
+ };
142
+ }
143
+
144
+ async #upsertSession(
145
+ rdb: Database,
146
+ storage: Parameters<BackendTx["upsertSession"]>[0],
147
+ incarnation: string,
148
+ ): Promise<void> {
149
+ const row = sessionInsertColumns(storage, incarnation);
150
+ const header = sessionHeaderColumns(storage);
151
+ const table = sessions.table(rdb);
152
+ await table.insertIgnore(row, ["f_session_id"]);
153
+ await table.update(
154
+ header,
155
+ E.and(E.eq(table.f_session_id, storage.meta.id), headerChangedCondition(header)),
156
+ );
157
+ }
158
+
159
+ async #updateSeedLength(rdb: Database, id: string, seedLength: number): Promise<void> {
160
+ const table = sessions.table(rdb);
161
+ await table.update({ f_seed_length: seedLength }, E.eq(table.f_session_id, id));
162
+ }
163
+
164
+ async #refreshTitle(rdb: Database, id: string): Promise<void> {
165
+ const latest = await this.reads.latestTitleEvent(id);
166
+ const title = latest === undefined ? undefined : titleOfEventData(latest.fData);
167
+ const nextTitle = title ?? null;
168
+ const nextSeq = title === undefined ? null : latest!.fSequence;
169
+ const table = sessions.table(rdb);
170
+ await table.update(
171
+ { f_title: nextTitle, f_title_seq: nextSeq },
172
+ E.and(
173
+ E.eq(table.f_session_id, id),
174
+ E.or(E.isDistinctFrom(table.f_title, nextTitle), E.isDistinctFrom(table.f_title_seq, nextSeq)),
175
+ ),
176
+ );
177
+ }
178
+
179
+ async #insertEvents(
180
+ rdb: Database,
181
+ eventRows: EventInsert[],
182
+ usageRows: readonly EventUsageRow[],
183
+ ): Promise<void> {
184
+ if (eventRows.length === 0) return;
185
+ const eventsTable = events.table(rdb);
186
+ for (let i = 0; i < eventRows.length; i += INSERT_BATCH_ROWS) {
187
+ await eventsTable.insertMany(
188
+ eventRows.slice(i, i + INSERT_BATCH_ROWS).map((event) => ({
189
+ f_event_id: event.fEventId,
190
+ f_parent_id: event.fParentId,
191
+ f_type: event.fType,
192
+ f_kind: event.fKind,
193
+ f_role: event.fRole,
194
+ f_name: event.fName,
195
+ f_action_id: event.fActionId,
196
+ f_encoding: event.fEncoding,
197
+ f_data: event.fData,
198
+ f_created_at: event.fCreatedAt,
199
+ })),
200
+ );
201
+ }
202
+ if (usageRows.length > 0) {
203
+ const usage = eventUsage.table(rdb);
204
+ for (let i = 0; i < usageRows.length; i += INSERT_BATCH_ROWS) {
205
+ await usage.insertMany(
206
+ usageRows.slice(i, i + INSERT_BATCH_ROWS).map((row) => ({
207
+ f_event_id: row.fEventId,
208
+ f_created_at: row.fCreatedAt,
209
+ f_day: row.fDay,
210
+ f_provider: row.fProvider,
211
+ f_model: row.fModel,
212
+ f_referenced: row.fReferenced,
213
+ f_subagent: row.fSubagent,
214
+ f_input_tokens: row.fInputTokens,
215
+ f_output_tokens: row.fOutputTokens,
216
+ f_cache_read_tokens: row.fCacheReadTokens,
217
+ f_reasoning_tokens: row.fReasoningTokens,
218
+ f_total_tokens: row.fTotalTokens,
219
+ })),
220
+ { conflict: ["f_event_id"] },
221
+ );
222
+ }
223
+ }
224
+ }
225
+
226
+ async #insertBridges(
227
+ rdb: Database,
228
+ rows: Array<{ fSessionId: string; fEventId: string; fSequence: number; fSurfaceOp: string | null }>,
229
+ ): Promise<void> {
230
+ if (rows.length === 0) return;
231
+ const links = sessionEvents.table(rdb);
232
+ for (let i = 0; i < rows.length; i += INSERT_BATCH_ROWS) {
233
+ await links.insertMany(
234
+ rows.slice(i, i + INSERT_BATCH_ROWS).map((row) => ({
235
+ f_session_id: row.fSessionId,
236
+ f_event_id: row.fEventId,
237
+ f_sequence: row.fSequence,
238
+ f_surface_op: row.fSurfaceOp,
239
+ })),
240
+ );
241
+ }
242
+ }
243
+
244
+ async #updateHead(
245
+ rdb: Database,
246
+ id: string,
247
+ headEventId: string,
248
+ headSequence: number,
249
+ ): Promise<void> {
250
+ const table = sessions.table(rdb);
251
+ await table.update(
252
+ { f_head_event_id: headEventId, f_head_sequence: headSequence },
253
+ E.eq(table.f_session_id, id),
254
+ );
255
+ }
256
+
257
+ async #updateHeadAndRevision(
258
+ rdb: Database,
259
+ id: string,
260
+ headEventId: string,
261
+ headSequence: number,
262
+ lastEventAt?: number,
263
+ ): Promise<void> {
264
+ const table = sessions.table(rdb);
265
+ await table.update(
266
+ {
267
+ f_head_event_id: headEventId,
268
+ f_head_sequence: headSequence,
269
+ f_revision: E.add(table.f_revision, 1),
270
+ ...(lastEventAt === undefined ? {} : { f_last_event_at: lastEventAtExpr(lastEventAt) }),
271
+ },
272
+ E.eq(table.f_session_id, id),
273
+ );
274
+ }
275
+
276
+ async #bumpRevision(rdb: Database, id: string, lastEventAt?: number): Promise<void> {
277
+ const table = sessions.table(rdb);
278
+ await table.update(
279
+ {
280
+ f_revision: E.add(table.f_revision, 1),
281
+ ...(lastEventAt === undefined ? {} : { f_last_event_at: lastEventAtExpr(lastEventAt) }),
282
+ },
283
+ E.eq(table.f_session_id, id),
284
+ );
285
+ }
286
+
287
+ async #deleteBridgeTail(rdb: Database, id: string, fromSequence: number): Promise<readonly string[]> {
288
+ const links = sessionEvents.table(rdb);
289
+ const rows = await links.delete(
290
+ E.and(E.eq(links.f_session_id, id), E.gte(links.f_sequence, fromSequence)),
291
+ ["f_event_id"],
292
+ );
293
+ return rows.map((row) => row.f_event_id);
294
+ }
295
+
296
+ async #deleteSession(rdb: Database, id: string): Promise<readonly string[]> {
297
+ const links = sessionEvents.table(rdb);
298
+ const rows = await links.delete(E.eq(links.f_session_id, id), ["f_event_id"]);
299
+ await this.#deleteSessionStatsRows(rdb, [id]);
300
+ const table = sessions.table(rdb);
301
+ await table.delete(E.eq(table.f_session_id, id));
302
+ await this.#pruneWorkspace(rdb, [id]);
303
+ return rows.map((row) => row.f_event_id);
304
+ }
305
+
306
+ async #deleteSessions(
307
+ rdb: Database,
308
+ ids: string[],
309
+ ): Promise<{ deleted: number; eventIds: readonly string[] }> {
310
+ if (ids.length === 0) return { deleted: 0, eventIds: [] };
311
+ const links = sessionEvents.table(rdb);
312
+ const removed = await links.delete(E.inList(links.f_session_id, ids), ["f_event_id"]);
313
+ await this.#deleteSessionStatsRows(rdb, ids);
314
+ const table = sessions.table(rdb);
315
+ let deleted = 0;
316
+ for await (const row of table.select({ n: count() }).where(E.inList(table.f_session_id, ids))) {
317
+ deleted = Number(row.n);
318
+ }
319
+ await table.delete(E.inList(table.f_session_id, ids));
320
+ await this.#pruneWorkspace(rdb, ids);
321
+ return { deleted, eventIds: removed.map((row) => row.f_event_id) };
322
+ }
323
+
324
+ async #deleteSessionStatsRows(rdb: Database, ids: readonly string[]): Promise<void> {
325
+ for (const id of ids) await stats.deleteSessionStats(rdb, id);
326
+ }
327
+
328
+ async #pruneWorkspace(rdb: Database, ids: readonly string[]): Promise<void> {
329
+ await pruneWorkspaceDocuments(rdb, ids);
330
+ }
331
+ }
package/src/backend.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { SessionId } from "@deepseek-ai/dsh-session";
2
2
  import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
- import type { EventUsageRow } from "./log.ts";
3
+ import type { EventUsageRow } from "./log/usage-row.ts";
4
4
  import type { StorageRepository } from "./storage-takeover/types.ts";
5
5
  import type { UsageActivityTotals, UsageAggregate, UsageTokenTotals } from "./usage.ts";
6
6
 
@@ -90,6 +90,14 @@ export interface BackendTx {
90
90
 
91
91
  updateHead(id: SessionId, headEventId: string, headSequence: number): Promise<void>;
92
92
 
93
+ /** 推进 head 并 bump revision:同一行一次 UPDATE(分两次写是纯写放大)。 */
94
+ updateHeadAndRevision(
95
+ id: SessionId,
96
+ headEventId: string,
97
+ headSequence: number,
98
+ lastEventAt?: number,
99
+ ): Promise<void>;
100
+
93
101
  // 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。
94
102
  bumpRevision(id: SessionId, lastEventAt?: number): Promise<void>;
95
103
 
@@ -152,6 +160,23 @@ export interface SessionUsageBucket extends UsageTokenTotals {
152
160
  model: string | null;
153
161
  }
154
162
 
163
+ /** 会话读的共享实现:写路径之外的所有查询都在这里(两方言共用一套投影查询)。 */
164
+ export interface ReadModel {
165
+ getSession(id: string): Promise<SessionRow | undefined>;
166
+ getEventRows(id: string, fromSequence?: number): Promise<EventRow[]>;
167
+ getEventTypeAt(id: string, sequence: number): Promise<string | undefined>;
168
+ getEventTypesBefore(
169
+ id: string,
170
+ beforeSequence: number,
171
+ limit: number,
172
+ ): Promise<Array<Pick<EventRow, "fSequence" | "fType">>>;
173
+ listSessions(): Promise<SessionRow[]>;
174
+ /** 最新一条 `session/title` 事件(`refreshTitle` 用)。 */
175
+ latestTitleEvent(id: string): Promise<{ fSequence: number; fData: string } | undefined>;
176
+ /** 父会话已不在表里的 subagent 会话。 */
177
+ listOrphanSubagentSessions(): Promise<string[]>;
178
+ }
179
+
155
180
  export interface Backend {
156
181
  readonly kind: "sqlite" | "postgres";
157
182
 
@@ -159,21 +184,10 @@ export interface Backend {
159
184
 
160
185
  readonly storage: StorageRepository;
161
186
 
162
- open(): Promise<void>;
163
-
164
- getSession(id: SessionId): Promise<SessionRow | undefined>;
187
+ /** 读面:会话行与事件行的查询(不再是 Backend 自己的 6 个转发方法)。 */
188
+ readonly reads: ReadModel;
165
189
 
166
- getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]>;
167
-
168
- getEventTypeAt(id: SessionId, sequence: number): Promise<string | undefined>;
169
-
170
- getEventTypesBefore(
171
- id: SessionId,
172
- beforeSequence: number,
173
- limit: number,
174
- ): Promise<Array<Pick<EventRow, "fSequence" | "fType">>>;
175
-
176
- listSessions(): Promise<SessionRow[]>;
190
+ open(): Promise<void>;
177
191
 
178
192
  transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T>;
179
193
 
@@ -201,6 +215,9 @@ export interface Backend {
201
215
  // 按会话从事件表重算两张汇总表(旧日志重写之后调用:迁移进来的会话没有旁路累加的历史,得补算)。
202
216
  rebuildSessionStats(id: SessionId): Promise<void>;
203
217
 
218
+ // 三张派生统计表清空后全量重算(整理数据的一步);返回涉及的会话数。
219
+ rebuildAllStats(): Promise<number>;
220
+
204
221
  // 重算这些用量行的引用标记(`f_referenced` / `f_subagent`):会话删除 / 日志重写之后调用,
205
222
  // `eventIds` 是被删掉的那批桥接行引用的事件(只有它们的引用可能消失)。
206
223
  // fork 与撤回(rewind)都**不**走这里——它们不改变已发生过的消耗(见 ADR-fork不继承统计、ADR-撤回不回退统计)。