@morlay/session-rdb 0.1.9 → 0.1.10-alpha.2

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 (143) hide show
  1. package/README.md +24 -3
  2. package/dist/artifact.d.mts +118 -3
  3. package/dist/artifact.mjs +5 -3
  4. package/dist/{log-C0q5svm7.d.mts → backend-BKtJ5sO5.d.mts} +189 -291
  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 -3
  9. package/dist/deletion.mjs +2 -2
  10. package/dist/export-CfJQjQfr.mjs +123 -0
  11. package/dist/export.d.mts +1 -3
  12. package/dist/export.mjs +32 -57
  13. package/dist/import.d.mts +1 -3
  14. package/dist/import.mjs +32 -78
  15. package/dist/index-CUD2ZpTA.d.mts +71 -0
  16. package/dist/index.d.mts +20 -2
  17. package/dist/index.mjs +1562 -5
  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 -50
  23. package/dist/storage.mjs +3 -1
  24. package/dist/testing.d.mts +9 -5
  25. package/dist/testing.mjs +27 -8
  26. package/dist/usage-row-BQQM-_t6.mjs +36 -0
  27. package/dist/usage.d.mts +1 -1
  28. package/dist/usage.mjs +8 -42
  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} +125 -112
  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 -1630
  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 -272
  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-query.ts +17 -4
  74. package/src/session-store.ts +675 -0
  75. package/src/stats-fold.ts +44 -0
  76. package/src/storage-takeover/index.ts +2 -5
  77. package/src/storage-takeover/legacy-domain-migration.ts +118 -0
  78. package/src/storage-takeover/projection-cache.ts +139 -77
  79. package/src/storage-takeover/read-model.ts +189 -0
  80. package/src/storage-takeover/repository.ts +238 -425
  81. package/src/storage-takeover/storage-backend.ts +47 -149
  82. package/src/storage-takeover/types.ts +45 -14
  83. package/src/storage-takeover/workspace-documents.ts +65 -0
  84. package/src/storage.ts +4 -2
  85. package/src/testing.ts +21 -1
  86. package/src/usage.ts +11 -65
  87. package/src/version.ts +13 -0
  88. package/src/write-path.ts +286 -0
  89. package/dist/branch-DbFW3rJX.mjs +0 -391
  90. package/dist/deletion-ODZFcbZm.mjs +0 -2218
  91. package/dist/gc.d.mts +0 -6
  92. package/dist/gc.mjs +0 -49
  93. package/dist/log-DYxdjJ-l.mjs +0 -562
  94. package/dist/routes-DGBlBE_I.mjs +0 -10
  95. package/dist/sqlite-DjOnMRh2.mjs +0 -1777
  96. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
  97. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
  98. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
  99. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
  100. package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
  101. package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
  102. package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
  103. package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
  104. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
  105. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
  106. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
  107. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
  108. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
  109. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
  110. package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
  111. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
  112. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
  113. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
  114. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
  115. package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
  116. package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
  117. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
  118. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
  119. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
  120. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
  121. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
  122. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
  123. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
  124. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
  125. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
  126. package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
  127. package/src/adapters/index.ts +0 -12
  128. package/src/adapters/to-postgres.ts +0 -93
  129. package/src/adapters/to-sqlite.ts +0 -83
  130. package/src/adapters/types.ts +0 -44
  131. package/src/drizzle/postgres-v2.ts +0 -10
  132. package/src/drizzle/postgres-v3.ts +0 -18
  133. package/src/drizzle/sqlite-v2.ts +0 -9
  134. package/src/drizzle/sqlite-v3.ts +0 -18
  135. package/src/entities/v2/index.ts +0 -20
  136. package/src/entities/v2/session-events.ts +0 -10
  137. package/src/entities/v3/schedule-tasks.ts +0 -16
  138. package/src/entities/v3/workspace-sessions.ts +0 -19
  139. package/src/entities/v3/workspace-state.ts +0 -12
  140. package/src/entities/v3/workspaces.ts +0 -14
  141. package/src/gc.ts +0 -77
  142. package/src/postgres.ts +0 -1015
  143. package/src/sqlite.ts +0 -1086
@@ -0,0 +1,1341 @@
1
+ import { n as titleOfEventData, o as sessionHeaderColumns, s as sessionInsertColumns } from "./export-CfJQjQfr.mjs";
2
+ import { COUNTED_EVENT_TYPES, localDayKey } from "./usage.mjs";
3
+ import { E, and, count, defineCatalog, defineTable, eq, gte, isNull, lt, notInSubquery, or, sum } from "@morlay/rdbkit";
4
+ // src/version.ts
5
+ /** 库结构版本(写入 SQLite 的 `user_version`,不匹配即拒绝打开)。 */
6
+ const SCHEMA_VERSION = 3;
7
+ /** SQLite 文件头的 application id(`0x44534850` = "DSHP"),防止误开别的库。 */
8
+ const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 1146308688;
9
+ /** 事件 `f_data` 的编码(目前只有 JSON)。 */
10
+ const EVENT_ENCODING = "json";
11
+ /** SQLite 的默认 busy timeout(毫秒)。 */
12
+ const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
13
+ // src/entities/v3/persistence-state.ts
14
+ const persistenceState = defineTable({
15
+ name: "t_persistence_state",
16
+ columns: {
17
+ f_singleton: {
18
+ type: "integer",
19
+ primaryKey: true
20
+ },
21
+ f_store_id: {
22
+ type: "text",
23
+ notNull: true
24
+ }
25
+ },
26
+ checks: { ck_persistence_state_singleton: { expression: "f_singleton = 1" } }
27
+ });
28
+ // src/entities/v3/schema-meta.ts
29
+ const schemaMeta = defineTable({
30
+ name: "t_schema_meta",
31
+ columns: {
32
+ f_key: {
33
+ type: "text",
34
+ primaryKey: true
35
+ },
36
+ f_value: {
37
+ type: "text",
38
+ notNull: true
39
+ }
40
+ }
41
+ });
42
+ // src/entities/v3/sessions.ts
43
+ const sessions = defineTable({
44
+ name: "t_sessions",
45
+ columns: {
46
+ f_id: {
47
+ type: "serial",
48
+ primaryKey: true
49
+ },
50
+ f_session_id: {
51
+ type: "text",
52
+ notNull: true,
53
+ unique: true
54
+ },
55
+ f_head_event_id: {
56
+ type: "text",
57
+ notNull: true,
58
+ default: ""
59
+ },
60
+ f_head_sequence: {
61
+ type: "integer",
62
+ notNull: true,
63
+ default: -1
64
+ },
65
+ f_version: {
66
+ type: "integer",
67
+ notNull: true
68
+ },
69
+ f_created_at: {
70
+ type: "bigint",
71
+ notNull: true
72
+ },
73
+ f_cwd: { type: "text" },
74
+ f_parent_session: { type: "text" },
75
+ f_seed_length: { type: "integer" },
76
+ f_origin: { type: "text" },
77
+ f_delegation_depth: { type: "integer" },
78
+ f_agent_preset: { type: "text" },
79
+ f_incarnation: {
80
+ type: "text",
81
+ notNull: true
82
+ },
83
+ f_revision: {
84
+ type: "integer",
85
+ notNull: true
86
+ },
87
+ f_archived_at: { type: "bigint" },
88
+ f_pinned_seq: { type: "integer" },
89
+ f_last_event_at: { type: "bigint" },
90
+ f_title: { type: "text" },
91
+ f_title_seq: { type: "integer" }
92
+ }
93
+ });
94
+ // src/entities/v3/events.ts
95
+ const events = defineTable({
96
+ name: "t_events",
97
+ columns: {
98
+ f_id: {
99
+ type: "serial",
100
+ primaryKey: true
101
+ },
102
+ f_event_id: {
103
+ type: "text",
104
+ notNull: true,
105
+ unique: true
106
+ },
107
+ f_parent_id: {
108
+ type: "text",
109
+ notNull: true,
110
+ default: ""
111
+ },
112
+ f_type: {
113
+ type: "text",
114
+ notNull: true,
115
+ default: ""
116
+ },
117
+ f_kind: {
118
+ type: "text",
119
+ notNull: true,
120
+ default: ""
121
+ },
122
+ f_role: {
123
+ type: "text",
124
+ notNull: true,
125
+ default: ""
126
+ },
127
+ f_name: {
128
+ type: "text",
129
+ notNull: true,
130
+ default: ""
131
+ },
132
+ f_action_id: {
133
+ type: "text",
134
+ notNull: true,
135
+ default: ""
136
+ },
137
+ f_encoding: {
138
+ type: "text",
139
+ notNull: true,
140
+ default: ""
141
+ },
142
+ f_data: {
143
+ type: "text",
144
+ notNull: true
145
+ },
146
+ f_created_at: {
147
+ type: "bigint",
148
+ notNull: true,
149
+ default: 0
150
+ }
151
+ },
152
+ indexes: {
153
+ idx_events_kind: { columns: ["f_kind"] },
154
+ idx_events_role: { columns: ["f_role"] },
155
+ idx_events_name: { columns: ["f_name"] },
156
+ idx_events_action_id: { columns: ["f_action_id"] }
157
+ }
158
+ });
159
+ // src/entities/v3/session-events.ts
160
+ const sessionEvents = defineTable({
161
+ name: "t_session_events",
162
+ columns: {
163
+ f_id: {
164
+ type: "serial",
165
+ primaryKey: true
166
+ },
167
+ f_session_id: {
168
+ type: "text",
169
+ notNull: true,
170
+ references: {
171
+ table: "t_sessions",
172
+ column: "f_session_id",
173
+ onDelete: "cascade"
174
+ }
175
+ },
176
+ f_event_id: {
177
+ type: "text",
178
+ notNull: true,
179
+ references: {
180
+ table: "t_events",
181
+ column: "f_event_id",
182
+ onDelete: "cascade"
183
+ }
184
+ },
185
+ f_sequence: {
186
+ type: "integer",
187
+ notNull: true
188
+ },
189
+ f_surface_op: { type: "text" }
190
+ },
191
+ uniques: { uq_session_events_session_sequence: { columns: ["f_session_id", "f_sequence"] } },
192
+ indexes: { idx_session_events_event_id: { columns: ["f_event_id"] } }
193
+ });
194
+ // src/entities/v3/storage-units.ts
195
+ const storageUnits = defineTable({
196
+ name: "t_storage_units",
197
+ columns: {
198
+ f_name: {
199
+ type: "text",
200
+ primaryKey: true
201
+ },
202
+ f_version: {
203
+ type: "integer",
204
+ notNull: true
205
+ }
206
+ }
207
+ });
208
+ // src/entities/v3/storage-records.ts
209
+ const storageRecords = defineTable({
210
+ name: "t_storage_records",
211
+ columns: {
212
+ f_domain: {
213
+ type: "text",
214
+ notNull: true
215
+ },
216
+ f_table: {
217
+ type: "text",
218
+ notNull: true
219
+ },
220
+ f_key: {
221
+ type: "text",
222
+ notNull: true
223
+ },
224
+ f_value: {
225
+ type: "text",
226
+ notNull: true
227
+ }
228
+ },
229
+ uniques: { uq_storage_records_domain_table_key: { columns: [
230
+ "f_domain",
231
+ "f_table",
232
+ "f_key"
233
+ ] } }
234
+ });
235
+ // src/entities/v3/session-projcache-rows.ts
236
+ const sessionProjcacheRows = defineTable({
237
+ name: "t_session_projcache_row",
238
+ columns: {
239
+ f_id: {
240
+ type: "serial",
241
+ primaryKey: true
242
+ },
243
+ f_session_id: {
244
+ type: "text",
245
+ notNull: true,
246
+ references: {
247
+ table: "t_sessions",
248
+ column: "f_session_id",
249
+ onDelete: "cascade"
250
+ }
251
+ },
252
+ f_key: {
253
+ type: "text",
254
+ notNull: true
255
+ },
256
+ f_ver: {
257
+ type: "integer",
258
+ notNull: true
259
+ },
260
+ f_seq: {
261
+ type: "integer",
262
+ notNull: true
263
+ },
264
+ f_val: {
265
+ type: "text",
266
+ notNull: true
267
+ }
268
+ },
269
+ uniques: { uq_session_projcache_row_session_key: { columns: ["f_session_id", "f_key"] } },
270
+ indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
271
+ });
272
+ // src/entities/v3/event-usage.ts
273
+ const eventUsage = defineTable({
274
+ name: "t_event_usage",
275
+ columns: {
276
+ f_event_id: {
277
+ type: "text",
278
+ primaryKey: true,
279
+ references: {
280
+ table: "t_events",
281
+ column: "f_event_id",
282
+ onDelete: "cascade"
283
+ }
284
+ },
285
+ f_created_at: {
286
+ type: "bigint",
287
+ notNull: true
288
+ },
289
+ f_day: {
290
+ type: "text",
291
+ notNull: true
292
+ },
293
+ f_provider: { type: "text" },
294
+ f_model: { type: "text" },
295
+ f_referenced: {
296
+ type: "integer",
297
+ notNull: true,
298
+ default: 0
299
+ },
300
+ f_subagent: {
301
+ type: "integer",
302
+ notNull: true,
303
+ default: 0
304
+ },
305
+ f_input_tokens: {
306
+ type: "integer",
307
+ notNull: true,
308
+ default: 0
309
+ },
310
+ f_output_tokens: {
311
+ type: "integer",
312
+ notNull: true,
313
+ default: 0
314
+ },
315
+ f_cache_read_tokens: {
316
+ type: "integer",
317
+ notNull: true,
318
+ default: 0
319
+ },
320
+ f_reasoning_tokens: {
321
+ type: "integer",
322
+ notNull: true,
323
+ default: 0
324
+ },
325
+ f_total_tokens: {
326
+ type: "integer",
327
+ notNull: true,
328
+ default: 0
329
+ }
330
+ },
331
+ indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
332
+ });
333
+ // src/entities/v3/session-usage.ts
334
+ const sessionUsage = defineTable({
335
+ name: "t_session_usage",
336
+ columns: {
337
+ f_session_id: {
338
+ type: "text",
339
+ notNull: true,
340
+ references: {
341
+ table: "t_sessions",
342
+ column: "f_session_id",
343
+ onDelete: "cascade"
344
+ }
345
+ },
346
+ f_day: {
347
+ type: "text",
348
+ notNull: true
349
+ },
350
+ f_provider: {
351
+ type: "text",
352
+ notNull: true,
353
+ default: ""
354
+ },
355
+ f_model: {
356
+ type: "text",
357
+ notNull: true,
358
+ default: ""
359
+ },
360
+ f_input_tokens: {
361
+ type: "integer",
362
+ notNull: true,
363
+ default: 0
364
+ },
365
+ f_output_tokens: {
366
+ type: "integer",
367
+ notNull: true,
368
+ default: 0
369
+ },
370
+ f_cache_read_tokens: {
371
+ type: "integer",
372
+ notNull: true,
373
+ default: 0
374
+ },
375
+ f_reasoning_tokens: {
376
+ type: "integer",
377
+ notNull: true,
378
+ default: 0
379
+ },
380
+ f_total_tokens: {
381
+ type: "integer",
382
+ notNull: true,
383
+ default: 0
384
+ }
385
+ },
386
+ uniques: { uq_session_usage_key: { columns: [
387
+ "f_session_id",
388
+ "f_day",
389
+ "f_provider",
390
+ "f_model"
391
+ ] } }
392
+ });
393
+ // src/entities/v3/session-counts.ts
394
+ const sessionCounts = defineTable({
395
+ name: "t_session_counts",
396
+ columns: {
397
+ f_session_id: {
398
+ type: "text",
399
+ notNull: true,
400
+ references: {
401
+ table: "t_sessions",
402
+ column: "f_session_id",
403
+ onDelete: "cascade"
404
+ }
405
+ },
406
+ f_day: {
407
+ type: "text",
408
+ notNull: true
409
+ },
410
+ f_turns: {
411
+ type: "integer",
412
+ notNull: true,
413
+ default: 0
414
+ },
415
+ f_steps: {
416
+ type: "integer",
417
+ notNull: true,
418
+ default: 0
419
+ },
420
+ f_user_inputs: {
421
+ type: "integer",
422
+ notNull: true,
423
+ default: 0
424
+ },
425
+ f_tool_calls: {
426
+ type: "integer",
427
+ notNull: true,
428
+ default: 0
429
+ }
430
+ },
431
+ uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } }
432
+ });
433
+ // src/entities/index.ts
434
+ /** 库结构的唯一事实来源(DDL、迁移与表句柄都从它算出来)。 */
435
+ const catalog = defineCatalog([
436
+ persistenceState,
437
+ schemaMeta,
438
+ sessions,
439
+ events,
440
+ sessionEvents,
441
+ storageUnits,
442
+ storageRecords,
443
+ sessionProjcacheRows,
444
+ eventUsage,
445
+ sessionUsage,
446
+ sessionCounts
447
+ ]);
448
+ // src/session-ops.ts
449
+ /**
450
+ * 会话读的小操作:经 `Database` 句柄调用——或为打开后的 base 实例,或为事务内的 scoped 实例,
451
+ * 执行面由调用方决定,这里不持有连接(与 Go 的 `session.T` 一致)。
452
+ */
453
+ /**
454
+ * upsert 累加:冲突时「列 = 列 + 新值」。
455
+ *
456
+ * 列引用必须带表名:PostgreSQL 的 `ON CONFLICT … DO UPDATE` 里裸列名会与 `excluded` 的同名列歧义
457
+ *(`column reference "f_turns" is ambiguous`),SQLite 两种写法都认。
458
+ */
459
+ function accumulated(table, column) {
460
+ return E.add(E.column(column, table), E.excluded(column));
461
+ }
462
+ /** 会话行的 head 指针(不存在时 fail loud)。 */
463
+ async function readHead(rdb, id) {
464
+ const table = sessions.table(rdb);
465
+ for await (const row of table.select({
466
+ fHeadEventId: table.f_head_event_id,
467
+ fHeadSequence: table.f_head_sequence
468
+ }).where(E.eq(table.f_session_id, id))) return row;
469
+ throw new Error(`session "${id}" has no materialized row`);
470
+ }
471
+ /** 会话行的 seed 长度(不存在时 fail loud)。 */
472
+ async function readSeedLength(rdb, id) {
473
+ const table = sessions.table(rdb);
474
+ for await (const row of table.select({ fSeedLength: table.f_seed_length }).where(E.eq(table.f_session_id, id))) return row.fSeedLength;
475
+ throw new Error(`session "${id}" has no materialized row`);
476
+ }
477
+ /** 指定序号上的桥接行(rewind 的边界探测)。 */
478
+ async function readPrevBridge(rdb, id, sequence) {
479
+ const links = sessionEvents.table(rdb);
480
+ for await (const row of links.select({
481
+ fEventId: links.f_event_id,
482
+ fSequence: links.f_sequence
483
+ }).where(E.and(E.eq(links.f_session_id, id), E.eq(links.f_sequence, sequence))).limit(1)) return row;
484
+ }
485
+ /** 回收没有任何桥接行的事件行(以及跟着走的用量行),返回回收的事件行数。 */
486
+ async function collectOrphans(rdb) {
487
+ const links = sessionEvents.table(rdb);
488
+ const eventsTable = events.table(rdb);
489
+ const orphans = [];
490
+ for await (const row of eventsTable.select({ id: eventsTable.f_event_id }).where(E.notInSubquery(eventsTable.f_event_id, links.select({ id: links.f_event_id }).toFragment()))) orphans.push(row.id);
491
+ if (orphans.length === 0) return 0;
492
+ await eventsTable.delete(E.inList(eventsTable.f_event_id, orphans));
493
+ const usageTable = eventUsage.table(rdb);
494
+ await usageTable.delete(E.notInSubquery(usageTable.f_event_id, eventsTable.select({ id: eventsTable.f_event_id }).toFragment()));
495
+ return orphans.length;
496
+ }
497
+ /**
498
+ * 重算用量行的两个归属标记(`f_referenced` / `f_subagent`)。
499
+ *
500
+ * 表达式是方言相关的(SQLite 直接用 `EXISTS`,PG 要 `::int`),由调用方给;分批是为了绑定参数上限。
501
+ */
502
+ async function refreshEventUsageFlags$1(rdb, eventIds, flags, chunkSize = 400) {
503
+ const usage = eventUsage.table(rdb);
504
+ for (let at = 0; at < eventIds.length; at += chunkSize) {
505
+ const chunk = eventIds.slice(at, at + chunkSize);
506
+ await usage.update({
507
+ f_referenced: flags.referenced,
508
+ f_subagent: flags.subagent
509
+ }, E.inList(usage.f_event_id, chunk));
510
+ }
511
+ }
512
+ /** 用量旁路累加:同键累加 token,不同键插新行(`ON CONFLICT … DO UPDATE SET 列 = 列 + excluded.列`)。 */
513
+ async function incrementSessionUsage$1(rdb, id, buckets) {
514
+ if (buckets.length === 0) return;
515
+ const usage = sessionUsage.table(rdb);
516
+ for (const bucket of buckets) await usage.upsertExpr({
517
+ f_session_id: id,
518
+ f_day: bucket.day,
519
+ f_provider: bucket.provider ?? "",
520
+ f_model: bucket.model ?? "",
521
+ f_input_tokens: bucket.inputTokens,
522
+ f_output_tokens: bucket.outputTokens,
523
+ f_cache_read_tokens: bucket.cacheReadTokens,
524
+ f_reasoning_tokens: bucket.reasoningTokens,
525
+ f_total_tokens: bucket.totalTokens
526
+ }, [
527
+ "f_session_id",
528
+ "f_day",
529
+ "f_provider",
530
+ "f_model"
531
+ ], {
532
+ f_input_tokens: accumulated(usage.name, "f_input_tokens"),
533
+ f_output_tokens: accumulated(usage.name, "f_output_tokens"),
534
+ f_cache_read_tokens: accumulated(usage.name, "f_cache_read_tokens"),
535
+ f_reasoning_tokens: accumulated(usage.name, "f_reasoning_tokens"),
536
+ f_total_tokens: accumulated(usage.name, "f_total_tokens")
537
+ });
538
+ }
539
+ /** 活动计数累加:同键累加,不同键插新行。 */
540
+ async function incrementSessionCounts$1(rdb, id, buckets) {
541
+ if (buckets.length === 0) return;
542
+ const counts = sessionCounts.table(rdb);
543
+ for (const bucket of buckets) await counts.upsertExpr({
544
+ f_session_id: id,
545
+ f_day: bucket.day,
546
+ f_turns: bucket.turns,
547
+ f_steps: bucket.steps,
548
+ f_user_inputs: bucket.userInputs,
549
+ f_tool_calls: bucket.toolCalls
550
+ }, ["f_session_id", "f_day"], {
551
+ f_turns: accumulated(counts.name, "f_turns"),
552
+ f_steps: accumulated(counts.name, "f_steps"),
553
+ f_user_inputs: accumulated(counts.name, "f_user_inputs"),
554
+ f_tool_calls: accumulated(counts.name, "f_tool_calls")
555
+ });
556
+ }
557
+ /** 删掉一个会话的两张统计行(重建与删除路径共用)。 */
558
+ async function deleteSessionStats$1(rdb, id) {
559
+ const usage = sessionUsage.table(rdb);
560
+ await usage.delete(E.eq(usage.f_session_id, id));
561
+ const counts = sessionCounts.table(rdb);
562
+ await counts.delete(E.eq(counts.f_session_id, id));
563
+ }
564
+ /**
565
+ * 按事件行重算一个会话的两张统计表。
566
+ *
567
+ * 查询源由调用方按表句柄构造(本地日、JSON 提取都是 rdbkit 的表达式);插入列来自表定义。
568
+ */
569
+ async function rebuildSessionStats$1(rdb, id, selectSql) {
570
+ await deleteSessionStats$1(rdb, id);
571
+ await rdb.table(sessionUsage).insertFromSelect([
572
+ "f_session_id",
573
+ "f_day",
574
+ "f_provider",
575
+ "f_model",
576
+ "f_input_tokens",
577
+ "f_output_tokens",
578
+ "f_cache_read_tokens",
579
+ "f_reasoning_tokens",
580
+ "f_total_tokens"
581
+ ], selectSql.usage);
582
+ await rdb.table(sessionCounts).insertFromSelect([
583
+ "f_session_id",
584
+ "f_day",
585
+ "f_turns",
586
+ "f_steps",
587
+ "f_user_inputs",
588
+ "f_tool_calls"
589
+ ], selectSql.counts);
590
+ }
591
+ /** 用量总览的第一段:按「本地日 × 模型 × 是否子代理」聚合 token(只算被引用的用量行)。 */
592
+ async function usageBuckets(rdb, sinceMs) {
593
+ const usage = eventUsage.table(rdb);
594
+ const rows = [];
595
+ for await (const row of usage.select({
596
+ day: usage.f_day,
597
+ provider: usage.f_provider,
598
+ model: usage.f_model,
599
+ subagent: usage.f_subagent,
600
+ input_tokens: sum("f_input_tokens"),
601
+ output_tokens: sum("f_output_tokens"),
602
+ cache_read_tokens: sum("f_cache_read_tokens"),
603
+ reasoning_tokens: sum("f_reasoning_tokens"),
604
+ total_tokens: sum("f_total_tokens")
605
+ }).where(sinceMs === void 0 ? E.eq(usage.f_referenced, 1) : E.and(E.eq(usage.f_referenced, 1), E.gte(usage.f_created_at, sinceMs))).groupBy("f_day", "f_provider", "f_model", "f_subagent")) rows.push(row);
606
+ return rows;
607
+ }
608
+ /** 用量总览的第二段:按「是否子代理」聚合 token(人类 / 子代理两个口径)。 */
609
+ async function usageTokenScope(rdb, sinceMs) {
610
+ const usage = eventUsage.table(rdb);
611
+ const rows = [];
612
+ for await (const row of usage.select({
613
+ subagent: usage.f_subagent,
614
+ input_tokens: sum("f_input_tokens"),
615
+ output_tokens: sum("f_output_tokens"),
616
+ cache_read_tokens: sum("f_cache_read_tokens"),
617
+ reasoning_tokens: sum("f_reasoning_tokens"),
618
+ total_tokens: sum("f_total_tokens")
619
+ }).where(sinceMs === void 0 ? E.eq(usage.f_referenced, 1) : E.and(E.eq(usage.f_referenced, 1), E.gte(usage.f_created_at, sinceMs))).groupBy("f_subagent")) rows.push(row);
620
+ return rows;
621
+ }
622
+ /** 用量总览的第四段:按会话聚合 token(带上标题 / 是否子代理 / 是否归档)。 */
623
+ async function usageSessionTokens(rdb, sinceMs) {
624
+ const usage = sessionUsage.table(rdb);
625
+ const sessionsTable = sessions.table(rdb);
626
+ const rows = [];
627
+ for await (const row of usage.select({
628
+ session_id: usage.f_session_id,
629
+ title: sessionsTable.f_title,
630
+ subagent: E.toInt(E.eq(sessionsTable.f_origin, "subagent")),
631
+ archived: E.toInt(E.isNotNull(sessionsTable.f_archived_at)),
632
+ input_tokens: sum("f_input_tokens"),
633
+ output_tokens: sum("f_output_tokens"),
634
+ cache_read_tokens: sum("f_cache_read_tokens"),
635
+ reasoning_tokens: sum("f_reasoning_tokens"),
636
+ total_tokens: sum("f_total_tokens")
637
+ }).join(sessions, E.eq(usage.f_session_id, sessionsTable.f_session_id)).where(sinceMs === void 0 ? E.always() : E.gte(usage.f_day, localDayKey(sinceMs))).groupBy(usage.f_session_id, sessionsTable.f_title, sessionsTable.f_origin, sessionsTable.f_archived_at)) rows.push(row);
638
+ return rows;
639
+ }
640
+ /** 用量总览的第五段:按会话聚合活动计数。 */
641
+ async function usageSessionCounts(rdb, sinceMs) {
642
+ const counts = sessionCounts.table(rdb);
643
+ const rows = [];
644
+ for await (const row of counts.select({
645
+ session_id: counts.f_session_id,
646
+ turns: sum("f_turns"),
647
+ steps: sum("f_steps"),
648
+ user_inputs: sum("f_user_inputs"),
649
+ tool_calls: sum("f_tool_calls")
650
+ }).where(sinceMs === void 0 ? E.always() : E.gte(counts.f_day, localDayKey(sinceMs))).groupBy("f_session_id")) rows.push(row);
651
+ return rows;
652
+ }
653
+ /** 用量总览的第三段:按「是否子代理」聚合活动计数(只算有用量行的会话)。 */
654
+ async function usageActivityScope(rdb, sinceMs) {
655
+ const counts = sessionCounts.table(rdb);
656
+ const sessionsTable = sessions.table(rdb);
657
+ const usageSessions = sessionUsage.table(rdb);
658
+ const subagentExpr = E.toInt(E.eq(sessionsTable.f_origin, "subagent"));
659
+ const rows = [];
660
+ for await (const row of counts.select({
661
+ subagent: subagentExpr,
662
+ turns: sum("f_turns"),
663
+ steps: sum("f_steps"),
664
+ user_inputs: sum("f_user_inputs"),
665
+ tool_calls: sum("f_tool_calls")
666
+ }).join(sessions, E.eq(counts.f_session_id, sessionsTable.f_session_id)).where(E.and(E.exists(usageSessions.select({ one: usageSessions.f_session_id }).where(E.eq(usageSessions.f_session_id, counts.f_session_id)).toFragment()), sinceMs === void 0 ? E.always() : E.gte(counts.f_day, localDayKey(sinceMs)))).groupBy(sessionsTable.f_origin)) rows.push(row);
667
+ return rows;
668
+ }
669
+ /**
670
+ * 会话列表:分页返回会话行(含 workspace 标题由调用方补),并给出总数。
671
+ *
672
+ * 搜索是「标题模糊匹配 或 workspace 标题命中的会话 id」,排序用物化列
673
+ * `f_last_event_at`(没有事件的会话回落 createdAt)。
674
+ */
675
+ async function listSessionRows$1(rdb, args) {
676
+ const table = sessions.table(rdb);
677
+ const like = `%${args.needle}%`;
678
+ const search = args.needle === "" ? E.always() : E.like(E.lower(E.coalesce(table.f_title, "")), like);
679
+ const titleHit = args.titleMatches.length === 0 ? [] : [E.inList(table.f_session_id, args.titleMatches)];
680
+ const where = E.and(E.or(search, ...titleHit), args.includeSubagents ? E.always() : E.ne(E.coalesce(table.f_origin, ""), "subagent"));
681
+ const items = [];
682
+ for await (const row of table.select({
683
+ session_id: table.f_session_id,
684
+ title: table.f_title,
685
+ origin: table.f_origin,
686
+ cwd: table.f_cwd,
687
+ created_at: table.f_created_at,
688
+ archived: E.toInt(E.isNotNull(table.f_archived_at)),
689
+ updated_at: E.coalesce(table.f_last_event_at, table.f_created_at)
690
+ }).where(where).orderBy(E.column("updated_at"), "desc").orderBy(table.f_session_id).limit(args.limit).offset(args.offset)) items.push(row);
691
+ let total = 0;
692
+ for await (const row of table.select({ n: count() }).where(where)) total = Number(row.n);
693
+ return {
694
+ items,
695
+ total
696
+ };
697
+ }
698
+ // src/storage-takeover/read-model.ts
699
+ function createReadModel(database) {
700
+ const sessionsTable = sessions.table(database);
701
+ const eventsTable = events.table(database);
702
+ const links = sessionEvents.table(database);
703
+ return {
704
+ async getSession(id) {
705
+ for await (const row of sessionsTable.select().where(eq(sessionsTable.f_session_id, id))) return sessionRowOf(row);
706
+ },
707
+ async getEventRows(id, fromSequence) {
708
+ const rows = [];
709
+ const condition = fromSequence === void 0 ? eq(links.f_session_id, id) : and(eq(links.f_session_id, id), gte(links.f_sequence, fromSequence));
710
+ for await (const row of links.select({
711
+ fEventId: links.f_event_id,
712
+ fSequence: links.f_sequence,
713
+ fType: eventsTable.f_type,
714
+ fKind: eventsTable.f_kind,
715
+ fRole: eventsTable.f_role,
716
+ fName: eventsTable.f_name,
717
+ fActionId: eventsTable.f_action_id,
718
+ fCreatedAt: eventsTable.f_created_at,
719
+ fData: eventsTable.f_data,
720
+ fSurfaceOp: links.f_surface_op
721
+ }).join(events, eq(eventsTable.f_event_id, links.f_event_id)).where(condition).orderBy("f_sequence")) rows.push(eventRowOf(row));
722
+ return rows;
723
+ },
724
+ async getEventTypeAt(id, sequence) {
725
+ for await (const row of links.select({ fType: eventsTable.f_type }).join(events, eq(eventsTable.f_event_id, links.f_event_id)).where(and(eq(links.f_session_id, id), eq(links.f_sequence, sequence))).limit(1)) return row.fType;
726
+ },
727
+ async getEventTypesBefore(id, beforeSequence, limit) {
728
+ const rows = [];
729
+ for await (const row of links.select({
730
+ fSequence: links.f_sequence,
731
+ fType: eventsTable.f_type
732
+ }).join(events, eq(eventsTable.f_event_id, links.f_event_id)).where(and(eq(links.f_session_id, id), lt(links.f_sequence, beforeSequence))).orderBy("f_sequence", "desc").limit(limit)) rows.push({
733
+ fSequence: Number(row.fSequence),
734
+ fType: row.fType
735
+ });
736
+ return rows;
737
+ },
738
+ async listSessions() {
739
+ const rows = [];
740
+ for await (const row of sessionsTable.select()) rows.push(sessionRowOf(row));
741
+ return rows;
742
+ },
743
+ async latestTitleEvent(id) {
744
+ for await (const row of links.select({
745
+ fSequence: links.f_sequence,
746
+ fData: eventsTable.f_data
747
+ }).join(events, eq(eventsTable.f_event_id, links.f_event_id)).where(and(eq(links.f_session_id, id), eq(eventsTable.f_type, "session/title"))).orderBy("f_sequence", "desc").limit(1)) return {
748
+ fSequence: Number(row.fSequence),
749
+ fData: row.fData
750
+ };
751
+ },
752
+ async listOrphanSubagentSessions() {
753
+ const ids = [];
754
+ for await (const row of sessionsTable.select({ id: sessionsTable.f_session_id }).where(and(eq(sessionsTable.f_origin, "subagent"), or(isNull(sessionsTable.f_parent_session), notInSubquery(sessionsTable.f_parent_session, sessionsTable.select({ id: sessionsTable.f_session_id }).toFragment()))))) ids.push(row.id);
755
+ return ids;
756
+ }
757
+ };
758
+ }
759
+ /** `f_last_event_at` 的「只增不减」(两方言同一份表达式,值走绑定)。 */
760
+ function lastEventAtExpr(value) {
761
+ return E.maxOf(E.column("f_last_event_at"), value);
762
+ }
763
+ /** header 的「变了才写」条件:逐列 `IS DISTINCT FROM` 的 OR(值全部走绑定参数)。 */
764
+ function headerChangedCondition(header) {
765
+ return E.or(...Object.entries(header).map(([name, value]) => E.isDistinctFrom(E.column(name, "t_sessions"), value)));
766
+ }
767
+ /**
768
+ * 事件行的数值列统一收回 number:`f_created_at` 在表定义里是 `bigint`(rdbkit 读回 BigInt),
769
+ * 事件坐标与毫秒时间都在安全整数范围内。
770
+ */
771
+ function eventRowOf(row) {
772
+ return {
773
+ fEventId: row.fEventId,
774
+ fSequence: Number(row.fSequence),
775
+ fType: row.fType,
776
+ fKind: row.fKind,
777
+ fRole: row.fRole,
778
+ fName: row.fName,
779
+ fActionId: row.fActionId,
780
+ fCreatedAt: Number(row.fCreatedAt),
781
+ fData: row.fData,
782
+ fSurfaceOp: row.fSurfaceOp
783
+ };
784
+ }
785
+ /** 列名原样 → 会话行的 camelCase(列名由表定义固定,这里是唯一一处映射)。 */
786
+ function sessionRowOf(row) {
787
+ return {
788
+ fSessionId: String(row["f_session_id"]),
789
+ fHeadEventId: String(row["f_head_event_id"]),
790
+ fHeadSequence: Number(row["f_head_sequence"]),
791
+ fVersion: Number(row["f_version"]),
792
+ fCreatedAt: Number(row["f_created_at"]),
793
+ fCwd: row["f_cwd"] ?? null,
794
+ fParentSession: row["f_parent_session"] ?? null,
795
+ fSeedLength: row["f_seed_length"] === null ? null : Number(row["f_seed_length"]),
796
+ fOrigin: row["f_origin"] ?? null,
797
+ fDelegationDepth: row["f_delegation_depth"] === null ? null : Number(row["f_delegation_depth"]),
798
+ fAgentPreset: row["f_agent_preset"] ?? null,
799
+ fIncarnation: String(row["f_incarnation"]),
800
+ fRevision: Number(row["f_revision"]),
801
+ fArchivedAt: row["f_archived_at"] === null ? null : Number(row["f_archived_at"]),
802
+ fPinnedSeq: row["f_pinned_seq"] === null ? null : Number(row["f_pinned_seq"]),
803
+ fLastEventAt: row["f_last_event_at"] === null ? null : Number(row["f_last_event_at"])
804
+ };
805
+ }
806
+ // src/db/stats.ts
807
+ function asText(value) {
808
+ return typeof value === "string" ? value : null;
809
+ }
810
+ async function usageReport(database, sinceMs) {
811
+ const buckets = await usageBuckets(database, sinceMs);
812
+ const tokenScope = await usageTokenScope(database, sinceMs);
813
+ const activityScope = await usageActivityScope(database, sinceMs);
814
+ const tokenSessions = await usageSessionTokens(database, sinceMs);
815
+ const activitySessions = await usageSessionCounts(database, sinceMs);
816
+ const toToken = (row) => ({
817
+ inputTokens: Number(row["input_tokens"] ?? 0),
818
+ outputTokens: Number(row["output_tokens"] ?? 0),
819
+ cacheReadTokens: Number(row["cache_read_tokens"] ?? 0),
820
+ reasoningTokens: Number(row["reasoning_tokens"] ?? 0),
821
+ totalTokens: Number(row["total_tokens"] ?? 0)
822
+ });
823
+ const toActivity = (row) => ({
824
+ turns: Number(row["turns"] ?? 0),
825
+ steps: Number(row["steps"] ?? 0),
826
+ userInputs: Number(row["user_inputs"] ?? 0),
827
+ toolCalls: Number(row["tool_calls"] ?? 0)
828
+ });
829
+ const emptyToken = {
830
+ inputTokens: 0,
831
+ outputTokens: 0,
832
+ cacheReadTokens: 0,
833
+ reasoningTokens: 0,
834
+ totalTokens: 0
835
+ };
836
+ const emptyActivity = {
837
+ turns: 0,
838
+ steps: 0,
839
+ userInputs: 0,
840
+ toolCalls: 0
841
+ };
842
+ const addToken = (target, row) => {
843
+ const token = toToken(row);
844
+ target.inputTokens += token.inputTokens;
845
+ target.outputTokens += token.outputTokens;
846
+ target.cacheReadTokens += token.cacheReadTokens;
847
+ target.reasoningTokens += token.reasoningTokens;
848
+ target.totalTokens += token.totalTokens;
849
+ };
850
+ const addActivity = (target, row) => {
851
+ const activity = toActivity(row);
852
+ target.turns += activity.turns;
853
+ target.steps += activity.steps;
854
+ target.userInputs += activity.userInputs;
855
+ target.toolCalls += activity.toolCalls;
856
+ };
857
+ const sum = (left, right) => ({
858
+ inputTokens: left.inputTokens + right.inputTokens,
859
+ outputTokens: left.outputTokens + right.outputTokens,
860
+ cacheReadTokens: left.cacheReadTokens + right.cacheReadTokens,
861
+ reasoningTokens: left.reasoningTokens + right.reasoningTokens,
862
+ totalTokens: left.totalTokens + right.totalTokens,
863
+ turns: left.turns + right.turns,
864
+ steps: left.steps + right.steps,
865
+ userInputs: left.userInputs + right.userInputs,
866
+ toolCalls: left.toolCalls + right.toolCalls
867
+ });
868
+ const human = {
869
+ ...emptyToken,
870
+ ...emptyActivity
871
+ };
872
+ const subagent = {
873
+ ...emptyToken,
874
+ ...emptyActivity
875
+ };
876
+ for (const row of tokenScope) addToken(Number(row.subagent) === 1 ? subagent : human, row);
877
+ for (const row of activityScope) addActivity(Number(row["subagent"]) === 1 ? subagent : human, row);
878
+ const activityBySession = /* @__PURE__ */ new Map();
879
+ for (const row of activitySessions) activityBySession.set(String(row["session_id"]), toActivity(row));
880
+ return {
881
+ totals: sum(sum({
882
+ ...emptyToken,
883
+ ...emptyActivity
884
+ }, human), subagent),
885
+ subagent,
886
+ human,
887
+ buckets: buckets.map((row) => ({
888
+ day: String(row.day),
889
+ provider: row.provider,
890
+ model: row.model,
891
+ subagent: Number(row.subagent) === 1,
892
+ ...toToken(row)
893
+ })),
894
+ sessions: tokenSessions.map((row) => ({
895
+ sessionId: String(row["session_id"]),
896
+ title: asText(row["title"]),
897
+ subagent: Number(row["subagent"]) === 1 || row["subagent"] === true,
898
+ archived: Number(row["archived"]) === 1 || row["archived"] === true,
899
+ ...toToken(row),
900
+ ...activityBySession.get(String(row["session_id"])) ?? emptyActivity
901
+ }))
902
+ };
903
+ }
904
+ async function listSessionRows(database, storage, query) {
905
+ const needle = query?.query?.trim().toLowerCase() ?? "";
906
+ const index = await storage.listWorkspaceIndex();
907
+ const { items, total } = await listSessionRows$1(database, {
908
+ needle,
909
+ titleMatches: needle === "" ? [] : [...new Set(index.filter((entry) => entry.title.toLowerCase().includes(needle)).flatMap((entry) => entry.sessionIds))],
910
+ includeSubagents: query?.includeSubagents === true,
911
+ limit: query?.limit ?? 20,
912
+ offset: query?.offset ?? 0
913
+ });
914
+ const workspaceOf = /* @__PURE__ */ new Map();
915
+ for (const entry of index) for (const sessionId of entry.sessionIds) if (!workspaceOf.has(sessionId)) workspaceOf.set(sessionId, entry.title);
916
+ return {
917
+ items: items.map((row) => ({
918
+ sessionId: String(row["session_id"]),
919
+ title: asText(row["title"]),
920
+ origin: asText(row["origin"]),
921
+ cwd: asText(row["cwd"]),
922
+ createdAt: Number(row["created_at"]),
923
+ updatedAt: Number(row["updated_at"]),
924
+ archived: Number(row["archived"]) === 1 || row["archived"] === true,
925
+ subagent: row["origin"] === "subagent",
926
+ workspace: workspaceOf.get(String(row["session_id"])) ?? null
927
+ })),
928
+ total
929
+ };
930
+ }
931
+ async function incrementSessionUsage(database, id, buckets) {
932
+ await incrementSessionUsage$1(database, id, buckets);
933
+ }
934
+ async function incrementSessionCounts(database, id, buckets) {
935
+ await incrementSessionCounts$1(database, id, buckets);
936
+ }
937
+ async function deleteSessionStats(database, id) {
938
+ await deleteSessionStats$1(database, id);
939
+ }
940
+ async function rebuildSessionStats(database, id) {
941
+ const links = sessionEvents.table(database);
942
+ const eventsTable = events.table(database);
943
+ const usageTable = eventUsage.table(database);
944
+ await rebuildSessionStats$1(database, id, {
945
+ usage: links.select({
946
+ f_session_id: links.f_session_id,
947
+ f_day: usageTable.f_day,
948
+ f_provider: E.coalesce(usageTable.f_provider, ""),
949
+ f_model: E.coalesce(usageTable.f_model, ""),
950
+ f_input_tokens: sum("f_input_tokens", "t_event_usage"),
951
+ f_output_tokens: sum("f_output_tokens", "t_event_usage"),
952
+ f_cache_read_tokens: sum("f_cache_read_tokens", "t_event_usage"),
953
+ f_reasoning_tokens: sum("f_reasoning_tokens", "t_event_usage"),
954
+ f_total_tokens: sum("f_total_tokens", "t_event_usage")
955
+ }).join(eventUsage, E.eq(usageTable.f_event_id, links.f_event_id)).where(E.eq(links.f_session_id, id)).groupBy(links.f_session_id, usageTable.f_day, usageTable.f_provider, usageTable.f_model).toFragment(),
956
+ counts: links.select({
957
+ f_session_id: links.f_session_id,
958
+ f_day: E.localDay(eventsTable.f_created_at),
959
+ f_turns: E.countIf(E.eq(eventsTable.f_type, "turn/start")),
960
+ f_steps: E.countIf(E.eq(eventsTable.f_type, "step/start")),
961
+ f_user_inputs: E.countIf(E.eq(eventsTable.f_type, "user/message")),
962
+ f_tool_calls: E.countIf(E.eq(eventsTable.f_type, "tool/call"))
963
+ }).join(events, E.eq(eventsTable.f_event_id, links.f_event_id)).where(E.and(E.eq(links.f_session_id, id), E.inList(eventsTable.f_type, [...COUNTED_EVENT_TYPES]))).groupBy(links.f_session_id, E.localDay(eventsTable.f_created_at)).toFragment()
964
+ });
965
+ }
966
+ async function refreshEventUsageFlags(database, eventIds) {
967
+ const links = sessionEvents.table(database);
968
+ const sessionsTable = sessions.table(database);
969
+ const refersTo = (eventId) => E.eq(links.f_event_id, eventId);
970
+ await refreshEventUsageFlags$1(database, eventIds, {
971
+ referenced: E.toInt(E.exists(links.select({ one: links.f_event_id }).where(refersTo(E.column("f_event_id", "t_event_usage"))).toFragment())),
972
+ subagent: E.toInt(E.exists(links.select({ one: links.f_event_id }).join(sessions, E.eq(sessionsTable.f_session_id, links.f_session_id)).where(E.and(refersTo(E.column("f_event_id", "t_event_usage")), E.eq(sessionsTable.f_origin, "subagent"))).toFragment()))
973
+ }, 400);
974
+ }
975
+ /** 统计表为空时按事件表全量回填(幂等;迁移删表重建、手动清空后自愈)。 */
976
+ async function backfillUsageTables(database) {
977
+ const links = sessionEvents.table(database);
978
+ const eventsTable = events.table(database);
979
+ const sessionsTable = sessions.table(database);
980
+ const usageTable = eventUsage.table(database);
981
+ if (await isEmpty(database, eventUsage)) {
982
+ const hasUsage = (path) => E.isNotNull(E.jsonText(eventsTable.f_data, path));
983
+ const text = (path) => E.jsonText(eventsTable.f_data, path);
984
+ const int = (path) => E.coalesce(E.jsonInt(eventsTable.f_data, path), 0);
985
+ const usagePaths = (field) => [`$.data.usage.${field}`, `$.usage.${field}`];
986
+ const input = usagePaths("inputTokens");
987
+ const output = usagePaths("outputTokens");
988
+ const cacheRead = usagePaths("cacheReadTokens");
989
+ const reasoning = usagePaths("reasoningTokens");
990
+ const total = usagePaths("totalTokens");
991
+ const referenced = (eventId) => E.toInt(E.exists(links.select({ one: links.f_event_id }).where(E.eq(links.f_event_id, eventId)).toFragment()));
992
+ const subagent = (eventId) => E.toInt(E.exists(links.select({ one: links.f_event_id }).join(sessions, E.eq(sessionsTable.f_session_id, links.f_session_id)).where(E.and(E.eq(links.f_event_id, eventId), E.eq(sessionsTable.f_origin, "subagent"))).toFragment()));
993
+ const source = eventsTable.select({
994
+ f_event_id: eventsTable.f_event_id,
995
+ f_created_at: eventsTable.f_created_at,
996
+ f_day: E.localDay(eventsTable.f_created_at),
997
+ f_provider: E.coalesce(text("$.data.message.source.provider"), text("$.message.source.provider")),
998
+ f_model: E.coalesce(text("$.data.message.source.model"), text("$.message.source.model")),
999
+ f_referenced: referenced(E.column("f_event_id", "t_events")),
1000
+ f_subagent: subagent(E.column("f_event_id", "t_events")),
1001
+ f_input_tokens: E.coalesce(int(input[0]), int(input[1]), 0),
1002
+ f_output_tokens: E.coalesce(int(output[0]), int(output[1]), 0),
1003
+ f_cache_read_tokens: E.coalesce(int(cacheRead[0]), int(cacheRead[1]), 0),
1004
+ f_reasoning_tokens: E.coalesce(int(reasoning[0]), int(reasoning[1]), 0),
1005
+ f_total_tokens: E.coalesce(int(total[0]), int(total[1]), 0)
1006
+ }).where(E.and(E.eq(eventsTable.f_type, "assistant/message"), E.or(hasUsage("$.data.usage"), hasUsage("$.usage")))).toFragment();
1007
+ await usageTable.insertFromSelect([
1008
+ "f_event_id",
1009
+ "f_created_at",
1010
+ "f_day",
1011
+ "f_provider",
1012
+ "f_model",
1013
+ "f_referenced",
1014
+ "f_subagent",
1015
+ "f_input_tokens",
1016
+ "f_output_tokens",
1017
+ "f_cache_read_tokens",
1018
+ "f_reasoning_tokens",
1019
+ "f_total_tokens"
1020
+ ], source, { conflict: ["f_event_id"] });
1021
+ }
1022
+ if (await isEmpty(database, sessionUsage)) {
1023
+ const source = links.select({
1024
+ f_session_id: links.f_session_id,
1025
+ f_day: usageTable.f_day,
1026
+ f_provider: E.coalesce(usageTable.f_provider, ""),
1027
+ f_model: E.coalesce(usageTable.f_model, ""),
1028
+ f_input_tokens: sum("f_input_tokens", "t_event_usage"),
1029
+ f_output_tokens: sum("f_output_tokens", "t_event_usage"),
1030
+ f_cache_read_tokens: sum("f_cache_read_tokens", "t_event_usage"),
1031
+ f_reasoning_tokens: sum("f_reasoning_tokens", "t_event_usage"),
1032
+ f_total_tokens: sum("f_total_tokens", "t_event_usage")
1033
+ }).join(eventUsage, E.eq(usageTable.f_event_id, links.f_event_id)).groupBy(links.f_session_id, usageTable.f_day, usageTable.f_provider, usageTable.f_model).toFragment();
1034
+ await sessionUsage.table(database).insertFromSelect([
1035
+ "f_session_id",
1036
+ "f_day",
1037
+ "f_provider",
1038
+ "f_model",
1039
+ "f_input_tokens",
1040
+ "f_output_tokens",
1041
+ "f_cache_read_tokens",
1042
+ "f_reasoning_tokens",
1043
+ "f_total_tokens"
1044
+ ], source, { conflict: [
1045
+ "f_session_id",
1046
+ "f_day",
1047
+ "f_provider",
1048
+ "f_model"
1049
+ ] });
1050
+ }
1051
+ if (await isEmpty(database, sessionCounts)) {
1052
+ const source = links.select({
1053
+ f_session_id: links.f_session_id,
1054
+ f_day: E.localDay(eventsTable.f_created_at),
1055
+ f_turns: E.countIf(E.eq(eventsTable.f_type, "turn/start")),
1056
+ f_steps: E.countIf(E.eq(eventsTable.f_type, "step/start")),
1057
+ f_user_inputs: E.countIf(E.eq(eventsTable.f_type, "user/message")),
1058
+ f_tool_calls: E.countIf(E.eq(eventsTable.f_type, "tool/call"))
1059
+ }).join(events, E.eq(eventsTable.f_event_id, links.f_event_id)).where(E.inList(eventsTable.f_type, [...COUNTED_EVENT_TYPES])).groupBy(links.f_session_id, E.localDay(eventsTable.f_created_at)).toFragment();
1060
+ await sessionCounts.table(database).insertFromSelect([
1061
+ "f_session_id",
1062
+ "f_day",
1063
+ "f_turns",
1064
+ "f_steps",
1065
+ "f_user_inputs",
1066
+ "f_tool_calls"
1067
+ ], source, { conflict: ["f_session_id", "f_day"] });
1068
+ }
1069
+ }
1070
+ async function isEmpty(database, definition) {
1071
+ for await (const row of definition.table(database).select({ n: count() })) return Number(row.n) === 0;
1072
+ return true;
1073
+ }
1074
+ /**
1075
+ * 整理数据的一步:三张派生统计表**清空后全量重算**。
1076
+ *
1077
+ * 这三张表本来就是可销毁重建的派生数据(见 ADR-统计衍生表物化归属与汇总),所以重算走「删了再回填」
1078
+ * 这条与打开时自愈相同的路径,不逐会话补差。
1079
+ * @param database - 执行面。
1080
+ * @returns 重建涉及的会话数。
1081
+ */
1082
+ async function rebuildAllStats(database) {
1083
+ await sessionUsage.table(database).delete();
1084
+ await sessionCounts.table(database).delete();
1085
+ await eventUsage.table(database).delete();
1086
+ await backfillUsageTables(database);
1087
+ let total = 0;
1088
+ for await (const row of sessions.table(database).select({ n: count() })) total = Number(row.n);
1089
+ return total;
1090
+ }
1091
+ // src/storage-takeover/workspace-documents.ts
1092
+ const WORKSPACE_DOMAIN = "workspace";
1093
+ const WORKSPACE_TABLE = "workspaces";
1094
+ const GLOBAL_TABLE = "";
1095
+ /**
1096
+ * 删除会话后清掉 workspace 文档里的归属与归档 / 钉住集合。
1097
+ *
1098
+ * 域存储只有 `t_storage_records` 一份,没有再 join 的归属表,所以删除路径得读改文档。调用方保证在
1099
+ * 写事务内:SQLite 用同一连接上的 `Database`,PG 用当前事务的 scoped `Database`。
1100
+ * @param database - 当前执行面(事务内就是 scoped 实例)。
1101
+ * @param ids - 刚删除的会话 id。
1102
+ */
1103
+ async function pruneWorkspaceDocuments(database, ids) {
1104
+ if (ids.length === 0) return;
1105
+ const records = database.table(storageRecords);
1106
+ const scope = E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, WORKSPACE_TABLE));
1107
+ const drop = new Set(ids);
1108
+ for await (const row of records.select({
1109
+ key: records.f_key,
1110
+ value: records.f_value
1111
+ }).where(scope)) {
1112
+ const record = JSON.parse(row.value);
1113
+ const kept = record.sessionIds.filter((sessionId) => !drop.has(sessionId));
1114
+ if (kept.length === record.sessionIds.length) continue;
1115
+ await records.update({ f_value: JSON.stringify({
1116
+ ...record,
1117
+ sessionIds: kept
1118
+ }) }, E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, WORKSPACE_TABLE), E.eq(records.f_key, row.key)));
1119
+ }
1120
+ let globalValue;
1121
+ for await (const row of records.select({ value: records.f_value }).where(E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, GLOBAL_TABLE)))) globalValue = row.value;
1122
+ if (globalValue === void 0) return;
1123
+ const state = JSON.parse(globalValue);
1124
+ const archivedSessionIds = state.archivedSessionIds.filter((sessionId) => !drop.has(sessionId));
1125
+ const pinnedSessionIds = (state.pinnedSessionIds ?? []).filter((sessionId) => !drop.has(sessionId));
1126
+ if (archivedSessionIds.length === state.archivedSessionIds.length && pinnedSessionIds.length === (state.pinnedSessionIds ?? []).length) return;
1127
+ const next = {
1128
+ ...state,
1129
+ archivedSessionIds,
1130
+ pinnedSessionIds
1131
+ };
1132
+ await records.update({ f_value: JSON.stringify(next) }, E.and(E.eq(records.f_domain, WORKSPACE_DOMAIN), E.eq(records.f_table, GLOBAL_TABLE)));
1133
+ }
1134
+ // src/backend-rdb.ts
1135
+ const INSERT_BATCH_ROWS = 1e3;
1136
+ /**
1137
+ * 唯一的 backend 实现:读写方法收敛在这里(读模型 + `session-ops` + 表句柄);打开流程在 `db/open.ts`,
1138
+ * 统计面在 `db/stats.ts`。方言差异全部由 rdbkit 的表达式承担,这里不出现任何 SQL 文本。
1139
+ */
1140
+ var RdbBackend = class {
1141
+ storage;
1142
+ storeIdentity;
1143
+ #database;
1144
+ reads;
1145
+ constructor(options) {
1146
+ this.kind = options.kind;
1147
+ this.#database = options.database;
1148
+ this.storage = options.storage;
1149
+ this.reads = options.readModel;
1150
+ this.storeIdentity = options.storeIdentity;
1151
+ }
1152
+ kind;
1153
+ async open() {}
1154
+ async close() {
1155
+ await this.#database.close();
1156
+ }
1157
+ async transaction(fn) {
1158
+ return await this.#database.transaction(async (scoped) => fn(this.#txFor(scoped)));
1159
+ }
1160
+ async listOrphanSubagentSessions() {
1161
+ return await this.reads.listOrphanSubagentSessions();
1162
+ }
1163
+ /** 当前 head 指针(分支 / 测试读路径用;写路径在 `BackendTx` 里)。 */
1164
+ async getHead(id) {
1165
+ return await readHead(this.#database, id);
1166
+ }
1167
+ async collectOrphans() {
1168
+ return await collectOrphans(this.#database);
1169
+ }
1170
+ async usageReport(sinceMs) {
1171
+ return await usageReport(this.#database, sinceMs);
1172
+ }
1173
+ async listSessionRows(query) {
1174
+ return await listSessionRows(this.#database, this.storage, query);
1175
+ }
1176
+ async incrementSessionUsage(id, buckets) {
1177
+ await incrementSessionUsage(this.#database, id, buckets);
1178
+ }
1179
+ async incrementSessionCounts(id, buckets) {
1180
+ await incrementSessionCounts(this.#database, id, buckets);
1181
+ }
1182
+ async deleteSessionStats(id) {
1183
+ await deleteSessionStats(this.#database, id);
1184
+ }
1185
+ async rebuildSessionStats(id) {
1186
+ await rebuildSessionStats(this.#database, id);
1187
+ }
1188
+ async rebuildAllStats() {
1189
+ return await rebuildAllStats(this.#database);
1190
+ }
1191
+ async refreshEventUsageFlags(eventIds) {
1192
+ await refreshEventUsageFlags(this.#database, eventIds);
1193
+ }
1194
+ async vacuum() {
1195
+ await this.#database.vacuum();
1196
+ }
1197
+ #txFor(rdb) {
1198
+ return {
1199
+ upsertSession: (storage, incarnation) => this.#upsertSession(rdb, storage, incarnation),
1200
+ getHead: (id) => readHead(rdb, id),
1201
+ getSeedLength: (id) => readSeedLength(rdb, id),
1202
+ updateSeedLength: (id, seedLength) => this.#updateSeedLength(rdb, id, seedLength),
1203
+ refreshTitle: (id) => this.#refreshTitle(rdb, id),
1204
+ insertEvents: (events, usageRows) => this.#insertEvents(rdb, events, usageRows),
1205
+ insertBridges: (rows) => this.#insertBridges(rdb, rows),
1206
+ updateHead: (id, headEventId, headSequence) => this.#updateHead(rdb, id, headEventId, headSequence),
1207
+ updateHeadAndRevision: (id, headEventId, headSequence, lastEventAt) => this.#updateHeadAndRevision(rdb, id, headEventId, headSequence, lastEventAt),
1208
+ bumpRevision: (id, lastEventAt) => this.#bumpRevision(rdb, id, lastEventAt),
1209
+ deleteBridgeTail: (id, fromSequence) => this.#deleteBridgeTail(rdb, id, fromSequence),
1210
+ getPrevBridge: (id, sequence) => readPrevBridge(rdb, id, sequence),
1211
+ deleteSession: (id) => this.#deleteSession(rdb, id),
1212
+ deleteSessions: (ids) => this.#deleteSessions(rdb, ids)
1213
+ };
1214
+ }
1215
+ async #upsertSession(rdb, storage, incarnation) {
1216
+ const row = sessionInsertColumns(storage, incarnation);
1217
+ const header = sessionHeaderColumns(storage);
1218
+ const table = sessions.table(rdb);
1219
+ await table.insertIgnore(row, ["f_session_id"]);
1220
+ await table.update(header, E.and(E.eq(table.f_session_id, storage.meta.id), headerChangedCondition(header)));
1221
+ }
1222
+ async #updateSeedLength(rdb, id, seedLength) {
1223
+ const table = sessions.table(rdb);
1224
+ await table.update({ f_seed_length: seedLength }, E.eq(table.f_session_id, id));
1225
+ }
1226
+ async #refreshTitle(rdb, id) {
1227
+ const latest = await this.reads.latestTitleEvent(id);
1228
+ const title = latest === void 0 ? void 0 : titleOfEventData(latest.fData);
1229
+ const nextTitle = title ?? null;
1230
+ const nextSeq = title === void 0 ? null : latest.fSequence;
1231
+ const table = sessions.table(rdb);
1232
+ await table.update({
1233
+ f_title: nextTitle,
1234
+ f_title_seq: nextSeq
1235
+ }, E.and(E.eq(table.f_session_id, id), E.or(E.isDistinctFrom(table.f_title, nextTitle), E.isDistinctFrom(table.f_title_seq, nextSeq))));
1236
+ }
1237
+ async #insertEvents(rdb, eventRows, usageRows) {
1238
+ if (eventRows.length === 0) return;
1239
+ const eventsTable = events.table(rdb);
1240
+ for (let i = 0; i < eventRows.length; i += INSERT_BATCH_ROWS) await eventsTable.insertMany(eventRows.slice(i, i + INSERT_BATCH_ROWS).map((event) => ({
1241
+ f_event_id: event.fEventId,
1242
+ f_parent_id: event.fParentId,
1243
+ f_type: event.fType,
1244
+ f_kind: event.fKind,
1245
+ f_role: event.fRole,
1246
+ f_name: event.fName,
1247
+ f_action_id: event.fActionId,
1248
+ f_encoding: event.fEncoding,
1249
+ f_data: event.fData,
1250
+ f_created_at: event.fCreatedAt
1251
+ })));
1252
+ if (usageRows.length > 0) {
1253
+ const usage = eventUsage.table(rdb);
1254
+ for (let i = 0; i < usageRows.length; i += INSERT_BATCH_ROWS) await usage.insertMany(usageRows.slice(i, i + INSERT_BATCH_ROWS).map((row) => ({
1255
+ f_event_id: row.fEventId,
1256
+ f_created_at: row.fCreatedAt,
1257
+ f_day: row.fDay,
1258
+ f_provider: row.fProvider,
1259
+ f_model: row.fModel,
1260
+ f_referenced: row.fReferenced,
1261
+ f_subagent: row.fSubagent,
1262
+ f_input_tokens: row.fInputTokens,
1263
+ f_output_tokens: row.fOutputTokens,
1264
+ f_cache_read_tokens: row.fCacheReadTokens,
1265
+ f_reasoning_tokens: row.fReasoningTokens,
1266
+ f_total_tokens: row.fTotalTokens
1267
+ })), { conflict: ["f_event_id"] });
1268
+ }
1269
+ }
1270
+ async #insertBridges(rdb, rows) {
1271
+ if (rows.length === 0) return;
1272
+ const links = sessionEvents.table(rdb);
1273
+ for (let i = 0; i < rows.length; i += INSERT_BATCH_ROWS) await links.insertMany(rows.slice(i, i + INSERT_BATCH_ROWS).map((row) => ({
1274
+ f_session_id: row.fSessionId,
1275
+ f_event_id: row.fEventId,
1276
+ f_sequence: row.fSequence,
1277
+ f_surface_op: row.fSurfaceOp
1278
+ })));
1279
+ }
1280
+ async #updateHead(rdb, id, headEventId, headSequence) {
1281
+ const table = sessions.table(rdb);
1282
+ await table.update({
1283
+ f_head_event_id: headEventId,
1284
+ f_head_sequence: headSequence
1285
+ }, E.eq(table.f_session_id, id));
1286
+ }
1287
+ async #updateHeadAndRevision(rdb, id, headEventId, headSequence, lastEventAt) {
1288
+ const table = sessions.table(rdb);
1289
+ await table.update({
1290
+ f_head_event_id: headEventId,
1291
+ f_head_sequence: headSequence,
1292
+ f_revision: E.add(table.f_revision, 1),
1293
+ ...lastEventAt === void 0 ? {} : { f_last_event_at: lastEventAtExpr(lastEventAt) }
1294
+ }, E.eq(table.f_session_id, id));
1295
+ }
1296
+ async #bumpRevision(rdb, id, lastEventAt) {
1297
+ const table = sessions.table(rdb);
1298
+ await table.update({
1299
+ f_revision: E.add(table.f_revision, 1),
1300
+ ...lastEventAt === void 0 ? {} : { f_last_event_at: lastEventAtExpr(lastEventAt) }
1301
+ }, E.eq(table.f_session_id, id));
1302
+ }
1303
+ async #deleteBridgeTail(rdb, id, fromSequence) {
1304
+ const links = sessionEvents.table(rdb);
1305
+ return (await links.delete(E.and(E.eq(links.f_session_id, id), E.gte(links.f_sequence, fromSequence)), ["f_event_id"])).map((row) => row.f_event_id);
1306
+ }
1307
+ async #deleteSession(rdb, id) {
1308
+ const links = sessionEvents.table(rdb);
1309
+ const rows = await links.delete(E.eq(links.f_session_id, id), ["f_event_id"]);
1310
+ await this.#deleteSessionStatsRows(rdb, [id]);
1311
+ const table = sessions.table(rdb);
1312
+ await table.delete(E.eq(table.f_session_id, id));
1313
+ await this.#pruneWorkspace(rdb, [id]);
1314
+ return rows.map((row) => row.f_event_id);
1315
+ }
1316
+ async #deleteSessions(rdb, ids) {
1317
+ if (ids.length === 0) return {
1318
+ deleted: 0,
1319
+ eventIds: []
1320
+ };
1321
+ const links = sessionEvents.table(rdb);
1322
+ const removed = await links.delete(E.inList(links.f_session_id, ids), ["f_event_id"]);
1323
+ await this.#deleteSessionStatsRows(rdb, ids);
1324
+ const table = sessions.table(rdb);
1325
+ let deleted = 0;
1326
+ for await (const row of table.select({ n: count() }).where(E.inList(table.f_session_id, ids))) deleted = Number(row.n);
1327
+ await table.delete(E.inList(table.f_session_id, ids));
1328
+ await this.#pruneWorkspace(rdb, ids);
1329
+ return {
1330
+ deleted,
1331
+ eventIds: removed.map((row) => row.f_event_id)
1332
+ };
1333
+ }
1334
+ async #deleteSessionStatsRows(rdb, ids) {
1335
+ for (const id of ids) await deleteSessionStats(rdb, id);
1336
+ }
1337
+ async #pruneWorkspace(rdb, ids) {
1338
+ await pruneWorkspaceDocuments(rdb, ids);
1339
+ }
1340
+ };
1341
+ export { sessionProjcacheRows as a, sessions as c, EVENT_ENCODING as d, SCHEMA_VERSION as f, catalog as i, persistenceState as l, backfillUsageTables as n, storageRecords as o, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as p, createReadModel as r, storageUnits as s, RdbBackend as t, DEFAULT_BUSY_TIMEOUT_MS as u };