@morlay/session-rdb 0.1.10-alpha.0 → 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.
- package/README.md +18 -3
- package/dist/artifact.d.mts +118 -2
- package/dist/artifact.mjs +5 -3
- package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
- package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
- package/dist/branch-CG1PUlO6.mjs +833 -0
- package/dist/deletion-SSX7IqU8.mjs +35 -0
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -1
- package/dist/export.mjs +31 -55
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +31 -76
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1561 -4
- package/dist/open-YoyD_kah.mjs +561 -0
- package/dist/organize.d.mts +9 -0
- package/dist/organize.mjs +30 -0
- package/dist/route-DpfY0zq-.mjs +114 -0
- package/dist/storage.d.mts +41 -48
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -1
- package/dist/testing.mjs +25 -4
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +7 -40
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +10 -14
- package/src/artifact.ts +4 -1
- package/src/backend-rdb.ts +331 -0
- package/src/backend.ts +32 -15
- package/src/{branch.ts → branch/index.ts} +109 -110
- package/src/branch/port.ts +62 -0
- package/src/branch/semantics.ts +69 -0
- package/src/db/open.ts +180 -0
- package/src/db/stats.ts +449 -0
- package/src/deletion-error.ts +17 -0
- package/src/deletion.ts +19 -72
- package/src/entities/index.ts +16 -1
- package/src/entities/v3/event-usage.ts +3 -3
- package/src/entities/v3/events.ts +3 -3
- package/src/entities/v3/index.ts +4 -16
- package/src/entities/v3/persistence-state.ts +3 -3
- package/src/entities/v3/schema-meta.ts +3 -3
- package/src/entities/v3/session-counts.ts +3 -3
- package/src/entities/v3/session-events.ts +3 -3
- package/src/entities/v3/session-projcache-rows.ts +3 -3
- package/src/entities/v3/session-usage.ts +3 -3
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-records.ts +17 -0
- package/src/entities/v3/storage-units.ts +3 -3
- package/src/{schema.ts → event-shape.ts} +0 -46
- package/src/export.ts +32 -78
- package/src/handles.ts +349 -0
- package/src/http/route.ts +178 -0
- package/src/import-storages.ts +5 -2
- package/src/import.ts +63 -125
- package/src/index.ts +15 -1666
- package/src/legacy.ts +3 -1
- package/src/live-routing.ts +205 -0
- package/src/log/export.ts +84 -0
- package/src/{log.ts → log/repair.ts} +1 -268
- package/src/log/rows.ts +107 -0
- package/src/log/usage-row.ts +64 -0
- package/src/migrations.ts +196 -0
- package/src/organize.ts +38 -0
- package/src/read-path.ts +190 -0
- package/src/routes.ts +2 -1
- package/src/rows.ts +25 -77
- package/src/session-ops.ts +444 -0
- package/src/session-store.ts +675 -0
- package/src/stats-fold.ts +44 -0
- package/src/storage-takeover/index.ts +2 -5
- package/src/storage-takeover/legacy-domain-migration.ts +118 -0
- package/src/storage-takeover/projection-cache.ts +122 -71
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +236 -442
- package/src/storage-takeover/storage-backend.ts +44 -159
- package/src/storage-takeover/types.ts +45 -19
- package/src/storage-takeover/workspace-documents.ts +65 -0
- package/src/storage.ts +4 -2
- package/src/testing.ts +21 -1
- package/src/usage.ts +11 -65
- package/src/version.ts +13 -0
- package/src/write-path.ts +286 -0
- package/dist/branch-CoPCRKer.mjs +0 -401
- package/dist/deletion-CWl89TIR.mjs +0 -2249
- package/dist/gc.d.mts +0 -4
- package/dist/gc.mjs +0 -48
- package/dist/log-CcWy46VE.mjs +0 -558
- package/dist/routes-hZjTcX_R.mjs +0 -9
- package/dist/sqlite-BNRd1aqR.mjs +0 -1778
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
- package/src/adapters/index.ts +0 -12
- package/src/adapters/to-postgres.ts +0 -93
- package/src/adapters/to-sqlite.ts +0 -83
- package/src/adapters/types.ts +0 -44
- package/src/drizzle/postgres-v2.ts +0 -10
- package/src/drizzle/postgres-v3.ts +0 -18
- package/src/drizzle/sqlite-v2.ts +0 -9
- package/src/drizzle/sqlite-v3.ts +0 -18
- package/src/entities/v2/index.ts +0 -20
- package/src/entities/v2/session-events.ts +0 -10
- package/src/entities/v3/schedule-tasks.ts +0 -16
- package/src/entities/v3/workspace-sessions.ts +0 -19
- package/src/entities/v3/workspace-state.ts +0 -12
- package/src/entities/v3/workspaces.ts +0 -14
- package/src/gc.ts +0 -77
- package/src/postgres.ts +0 -1015
- package/src/sqlite.ts +0 -1096
|
@@ -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 };
|