@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.
- package/README.md +24 -3
- package/dist/artifact.d.mts +118 -3
- package/dist/artifact.mjs +5 -3
- package/dist/{log-C0q5svm7.d.mts → backend-BKtJ5sO5.d.mts} +189 -291
- 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 -3
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -3
- package/dist/export.mjs +32 -57
- package/dist/import.d.mts +1 -3
- package/dist/import.mjs +32 -78
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1562 -5
- 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 -50
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -5
- package/dist/testing.mjs +27 -8
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +8 -42
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +32 -36
- 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} +125 -112
- 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 -1630
- 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 -272
- 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-query.ts +17 -4
- 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 +139 -77
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +238 -425
- package/src/storage-takeover/storage-backend.ts +47 -149
- package/src/storage-takeover/types.ts +45 -14
- 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-DbFW3rJX.mjs +0 -391
- package/dist/deletion-ODZFcbZm.mjs +0 -2218
- package/dist/gc.d.mts +0 -6
- package/dist/gc.mjs +0 -49
- package/dist/log-DYxdjJ-l.mjs +0 -562
- package/dist/routes-DGBlBE_I.mjs +0 -10
- package/dist/sqlite-DjOnMRh2.mjs +0 -1777
- 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 -1086
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { count, E, sum, type Database, type ExprOperand, type Fragment } from "@morlay/rdbkit";
|
|
2
|
+
import type { SessionCountBucket, SessionRow, SessionUsageBucket } from "./backend.ts";
|
|
3
|
+
import { localDayKey } from "./usage.ts";
|
|
4
|
+
import {
|
|
5
|
+
eventUsage,
|
|
6
|
+
events,
|
|
7
|
+
sessionCounts,
|
|
8
|
+
sessionEvents,
|
|
9
|
+
sessions,
|
|
10
|
+
sessionUsage,
|
|
11
|
+
} from "./entities/index.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 会话读的小操作:经 `Database` 句柄调用——或为打开后的 base 实例,或为事务内的 scoped 实例,
|
|
15
|
+
* 执行面由调用方决定,这里不持有连接(与 Go 的 `session.T` 一致)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* upsert 累加:冲突时「列 = 列 + 新值」。
|
|
20
|
+
*
|
|
21
|
+
* 列引用必须带表名:PostgreSQL 的 `ON CONFLICT … DO UPDATE` 里裸列名会与 `excluded` 的同名列歧义
|
|
22
|
+
*(`column reference "f_turns" is ambiguous`),SQLite 两种写法都认。
|
|
23
|
+
*/
|
|
24
|
+
function accumulated(table: string, column: string): ExprOperand<number> {
|
|
25
|
+
return E.add(E.column(column, table), E.excluded(column));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 会话行的 head 指针(不存在时 fail loud)。 */
|
|
29
|
+
export async function readHead(
|
|
30
|
+
rdb: Database,
|
|
31
|
+
id: string,
|
|
32
|
+
): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
|
|
33
|
+
const table = sessions.table(rdb);
|
|
34
|
+
for await (const row of table
|
|
35
|
+
.select({ fHeadEventId: table.f_head_event_id, fHeadSequence: table.f_head_sequence })
|
|
36
|
+
.where(E.eq(table.f_session_id, id))) {
|
|
37
|
+
return row;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`session "${id}" has no materialized row`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 会话行的 seed 长度(不存在时 fail loud)。 */
|
|
43
|
+
export async function readSeedLength(rdb: Database, id: string): Promise<number | null> {
|
|
44
|
+
const table = sessions.table(rdb);
|
|
45
|
+
for await (const row of table
|
|
46
|
+
.select({ fSeedLength: table.f_seed_length })
|
|
47
|
+
.where(E.eq(table.f_session_id, id))) {
|
|
48
|
+
return row.fSeedLength;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`session "${id}" has no materialized row`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 指定序号上的桥接行(rewind 的边界探测)。 */
|
|
54
|
+
export async function readPrevBridge(
|
|
55
|
+
rdb: Database,
|
|
56
|
+
id: string,
|
|
57
|
+
sequence: number,
|
|
58
|
+
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
59
|
+
const links = sessionEvents.table(rdb);
|
|
60
|
+
for await (const row of links
|
|
61
|
+
.select({ fEventId: links.f_event_id, fSequence: links.f_sequence })
|
|
62
|
+
.where(E.and(E.eq(links.f_session_id, id), E.eq(links.f_sequence, sequence)))
|
|
63
|
+
.limit(1)) {
|
|
64
|
+
return row;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 回收没有任何桥接行的事件行(以及跟着走的用量行),返回回收的事件行数。 */
|
|
70
|
+
export async function collectOrphans(rdb: Database): Promise<number> {
|
|
71
|
+
const links = sessionEvents.table(rdb);
|
|
72
|
+
const eventsTable = events.table(rdb);
|
|
73
|
+
const orphans: string[] = [];
|
|
74
|
+
for await (const row of eventsTable
|
|
75
|
+
.select({ id: eventsTable.f_event_id })
|
|
76
|
+
.where(
|
|
77
|
+
E.notInSubquery(eventsTable.f_event_id, links.select({ id: links.f_event_id }).toFragment()),
|
|
78
|
+
)) {
|
|
79
|
+
orphans.push(row.id);
|
|
80
|
+
}
|
|
81
|
+
if (orphans.length === 0) return 0;
|
|
82
|
+
await eventsTable.delete(E.inList(eventsTable.f_event_id, orphans));
|
|
83
|
+
// 用量行跟着事件行走:没有事件行的用量不再计入统计。
|
|
84
|
+
const usageTable = eventUsage.table(rdb);
|
|
85
|
+
await usageTable.delete(
|
|
86
|
+
E.notInSubquery(
|
|
87
|
+
usageTable.f_event_id,
|
|
88
|
+
eventsTable.select({ id: eventsTable.f_event_id }).toFragment(),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
return orphans.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 重算用量行的两个归属标记(`f_referenced` / `f_subagent`)。
|
|
96
|
+
*
|
|
97
|
+
* 表达式是方言相关的(SQLite 直接用 `EXISTS`,PG 要 `::int`),由调用方给;分批是为了绑定参数上限。
|
|
98
|
+
*/
|
|
99
|
+
export async function refreshEventUsageFlags(
|
|
100
|
+
rdb: Database,
|
|
101
|
+
eventIds: readonly string[],
|
|
102
|
+
flags: { referenced: ExprOperand; subagent: ExprOperand },
|
|
103
|
+
chunkSize = 400,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const usage = eventUsage.table(rdb);
|
|
106
|
+
for (let at = 0; at < eventIds.length; at += chunkSize) {
|
|
107
|
+
const chunk = eventIds.slice(at, at + chunkSize);
|
|
108
|
+
await usage.update(
|
|
109
|
+
{ f_referenced: flags.referenced, f_subagent: flags.subagent },
|
|
110
|
+
E.inList(usage.f_event_id, chunk),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 用量旁路累加:同键累加 token,不同键插新行(`ON CONFLICT … DO UPDATE SET 列 = 列 + excluded.列`)。 */
|
|
116
|
+
export async function incrementSessionUsage(
|
|
117
|
+
rdb: Database,
|
|
118
|
+
id: string,
|
|
119
|
+
buckets: readonly SessionUsageBucket[],
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
if (buckets.length === 0) return;
|
|
122
|
+
const usage = sessionUsage.table(rdb);
|
|
123
|
+
for (const bucket of buckets) {
|
|
124
|
+
await usage.upsertExpr(
|
|
125
|
+
{
|
|
126
|
+
f_session_id: id,
|
|
127
|
+
f_day: bucket.day,
|
|
128
|
+
f_provider: bucket.provider ?? "",
|
|
129
|
+
f_model: bucket.model ?? "",
|
|
130
|
+
f_input_tokens: bucket.inputTokens,
|
|
131
|
+
f_output_tokens: bucket.outputTokens,
|
|
132
|
+
f_cache_read_tokens: bucket.cacheReadTokens,
|
|
133
|
+
f_reasoning_tokens: bucket.reasoningTokens,
|
|
134
|
+
f_total_tokens: bucket.totalTokens,
|
|
135
|
+
},
|
|
136
|
+
["f_session_id", "f_day", "f_provider", "f_model"],
|
|
137
|
+
{
|
|
138
|
+
f_input_tokens: accumulated(usage.name, "f_input_tokens"),
|
|
139
|
+
f_output_tokens: accumulated(usage.name, "f_output_tokens"),
|
|
140
|
+
f_cache_read_tokens: accumulated(usage.name, "f_cache_read_tokens"),
|
|
141
|
+
f_reasoning_tokens: accumulated(usage.name, "f_reasoning_tokens"),
|
|
142
|
+
f_total_tokens: accumulated(usage.name, "f_total_tokens"),
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 活动计数累加:同键累加,不同键插新行。 */
|
|
149
|
+
export async function incrementSessionCounts(
|
|
150
|
+
rdb: Database,
|
|
151
|
+
id: string,
|
|
152
|
+
buckets: readonly SessionCountBucket[],
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
if (buckets.length === 0) return;
|
|
155
|
+
const counts = sessionCounts.table(rdb);
|
|
156
|
+
for (const bucket of buckets) {
|
|
157
|
+
await counts.upsertExpr(
|
|
158
|
+
{
|
|
159
|
+
f_session_id: id,
|
|
160
|
+
f_day: bucket.day,
|
|
161
|
+
f_turns: bucket.turns,
|
|
162
|
+
f_steps: bucket.steps,
|
|
163
|
+
f_user_inputs: bucket.userInputs,
|
|
164
|
+
f_tool_calls: bucket.toolCalls,
|
|
165
|
+
},
|
|
166
|
+
["f_session_id", "f_day"],
|
|
167
|
+
{
|
|
168
|
+
f_turns: accumulated(counts.name, "f_turns"),
|
|
169
|
+
f_steps: accumulated(counts.name, "f_steps"),
|
|
170
|
+
f_user_inputs: accumulated(counts.name, "f_user_inputs"),
|
|
171
|
+
f_tool_calls: accumulated(counts.name, "f_tool_calls"),
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 删掉一个会话的两张统计行(重建与删除路径共用)。 */
|
|
178
|
+
export async function deleteSessionStats(rdb: Database, id: string): Promise<void> {
|
|
179
|
+
const usage = sessionUsage.table(rdb);
|
|
180
|
+
await usage.delete(E.eq(usage.f_session_id, id));
|
|
181
|
+
const counts = sessionCounts.table(rdb);
|
|
182
|
+
await counts.delete(E.eq(counts.f_session_id, id));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 按事件行重算一个会话的两张统计表。
|
|
187
|
+
*
|
|
188
|
+
* 查询源由调用方按表句柄构造(本地日、JSON 提取都是 rdbkit 的表达式);插入列来自表定义。
|
|
189
|
+
*/
|
|
190
|
+
export async function rebuildSessionStats(
|
|
191
|
+
rdb: Database,
|
|
192
|
+
id: string,
|
|
193
|
+
selectSql: { usage: Fragment; counts: Fragment },
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
await deleteSessionStats(rdb, id);
|
|
196
|
+
await rdb
|
|
197
|
+
.table(sessionUsage)
|
|
198
|
+
.insertFromSelect(
|
|
199
|
+
[
|
|
200
|
+
"f_session_id",
|
|
201
|
+
"f_day",
|
|
202
|
+
"f_provider",
|
|
203
|
+
"f_model",
|
|
204
|
+
"f_input_tokens",
|
|
205
|
+
"f_output_tokens",
|
|
206
|
+
"f_cache_read_tokens",
|
|
207
|
+
"f_reasoning_tokens",
|
|
208
|
+
"f_total_tokens",
|
|
209
|
+
],
|
|
210
|
+
selectSql.usage,
|
|
211
|
+
);
|
|
212
|
+
await rdb
|
|
213
|
+
.table(sessionCounts)
|
|
214
|
+
.insertFromSelect(
|
|
215
|
+
["f_session_id", "f_day", "f_turns", "f_steps", "f_user_inputs", "f_tool_calls"],
|
|
216
|
+
selectSql.counts,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 用量总览的第一段:按「本地日 × 模型 × 是否子代理」聚合 token(只算被引用的用量行)。 */
|
|
221
|
+
export async function usageBuckets(
|
|
222
|
+
rdb: Database,
|
|
223
|
+
sinceMs?: number,
|
|
224
|
+
): Promise<
|
|
225
|
+
Array<{
|
|
226
|
+
day: string;
|
|
227
|
+
provider: string | null;
|
|
228
|
+
model: string | null;
|
|
229
|
+
subagent: number;
|
|
230
|
+
input_tokens: number | bigint;
|
|
231
|
+
output_tokens: number | bigint;
|
|
232
|
+
cache_read_tokens: number | bigint;
|
|
233
|
+
reasoning_tokens: number | bigint;
|
|
234
|
+
total_tokens: number | bigint;
|
|
235
|
+
}>
|
|
236
|
+
> {
|
|
237
|
+
const usage = eventUsage.table(rdb);
|
|
238
|
+
const rows = [];
|
|
239
|
+
for await (const row of usage
|
|
240
|
+
.select({
|
|
241
|
+
day: usage.f_day,
|
|
242
|
+
provider: usage.f_provider,
|
|
243
|
+
model: usage.f_model,
|
|
244
|
+
subagent: usage.f_subagent,
|
|
245
|
+
input_tokens: sum("f_input_tokens"),
|
|
246
|
+
output_tokens: sum("f_output_tokens"),
|
|
247
|
+
cache_read_tokens: sum("f_cache_read_tokens"),
|
|
248
|
+
reasoning_tokens: sum("f_reasoning_tokens"),
|
|
249
|
+
total_tokens: sum("f_total_tokens"),
|
|
250
|
+
})
|
|
251
|
+
.where(
|
|
252
|
+
sinceMs === undefined
|
|
253
|
+
? E.eq(usage.f_referenced, 1)
|
|
254
|
+
: E.and(E.eq(usage.f_referenced, 1), E.gte(usage.f_created_at, sinceMs)),
|
|
255
|
+
)
|
|
256
|
+
.groupBy("f_day", "f_provider", "f_model", "f_subagent")) {
|
|
257
|
+
rows.push(row);
|
|
258
|
+
}
|
|
259
|
+
return rows;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 用量总览的第二段:按「是否子代理」聚合 token(人类 / 子代理两个口径)。 */
|
|
263
|
+
export async function usageTokenScope(
|
|
264
|
+
rdb: Database,
|
|
265
|
+
sinceMs?: number,
|
|
266
|
+
): Promise<
|
|
267
|
+
Array<{
|
|
268
|
+
subagent: number;
|
|
269
|
+
input_tokens: number | bigint;
|
|
270
|
+
output_tokens: number | bigint;
|
|
271
|
+
cache_read_tokens: number | bigint;
|
|
272
|
+
reasoning_tokens: number | bigint;
|
|
273
|
+
total_tokens: number | bigint;
|
|
274
|
+
}>
|
|
275
|
+
> {
|
|
276
|
+
const usage = eventUsage.table(rdb);
|
|
277
|
+
const rows = [];
|
|
278
|
+
for await (const row of usage
|
|
279
|
+
.select({
|
|
280
|
+
subagent: usage.f_subagent,
|
|
281
|
+
input_tokens: sum("f_input_tokens"),
|
|
282
|
+
output_tokens: sum("f_output_tokens"),
|
|
283
|
+
cache_read_tokens: sum("f_cache_read_tokens"),
|
|
284
|
+
reasoning_tokens: sum("f_reasoning_tokens"),
|
|
285
|
+
total_tokens: sum("f_total_tokens"),
|
|
286
|
+
})
|
|
287
|
+
.where(
|
|
288
|
+
sinceMs === undefined
|
|
289
|
+
? E.eq(usage.f_referenced, 1)
|
|
290
|
+
: E.and(E.eq(usage.f_referenced, 1), E.gte(usage.f_created_at, sinceMs)),
|
|
291
|
+
)
|
|
292
|
+
.groupBy("f_subagent")) {
|
|
293
|
+
rows.push(row);
|
|
294
|
+
}
|
|
295
|
+
return rows;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** 用量总览的第四段:按会话聚合 token(带上标题 / 是否子代理 / 是否归档)。 */
|
|
299
|
+
export async function usageSessionTokens(
|
|
300
|
+
rdb: Database,
|
|
301
|
+
sinceMs?: number,
|
|
302
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
303
|
+
const usage = sessionUsage.table(rdb);
|
|
304
|
+
const sessionsTable = sessions.table(rdb);
|
|
305
|
+
const rows = [];
|
|
306
|
+
for await (const row of usage
|
|
307
|
+
.select({
|
|
308
|
+
session_id: usage.f_session_id,
|
|
309
|
+
title: sessionsTable.f_title,
|
|
310
|
+
subagent: E.toInt(E.eq(sessionsTable.f_origin, "subagent")),
|
|
311
|
+
archived: E.toInt(E.isNotNull(sessionsTable.f_archived_at)),
|
|
312
|
+
input_tokens: sum("f_input_tokens"),
|
|
313
|
+
output_tokens: sum("f_output_tokens"),
|
|
314
|
+
cache_read_tokens: sum("f_cache_read_tokens"),
|
|
315
|
+
reasoning_tokens: sum("f_reasoning_tokens"),
|
|
316
|
+
total_tokens: sum("f_total_tokens"),
|
|
317
|
+
})
|
|
318
|
+
.join(sessions, E.eq(usage.f_session_id, sessionsTable.f_session_id))
|
|
319
|
+
.where(sinceMs === undefined ? E.always() : E.gte(usage.f_day, localDayKey(sinceMs)))
|
|
320
|
+
.groupBy(
|
|
321
|
+
usage.f_session_id,
|
|
322
|
+
sessionsTable.f_title,
|
|
323
|
+
sessionsTable.f_origin,
|
|
324
|
+
sessionsTable.f_archived_at,
|
|
325
|
+
)) {
|
|
326
|
+
rows.push(row);
|
|
327
|
+
}
|
|
328
|
+
return rows;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 用量总览的第五段:按会话聚合活动计数。 */
|
|
332
|
+
export async function usageSessionCounts(
|
|
333
|
+
rdb: Database,
|
|
334
|
+
sinceMs?: number,
|
|
335
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
336
|
+
const counts = sessionCounts.table(rdb);
|
|
337
|
+
const rows = [];
|
|
338
|
+
for await (const row of counts
|
|
339
|
+
.select({
|
|
340
|
+
session_id: counts.f_session_id,
|
|
341
|
+
turns: sum("f_turns"),
|
|
342
|
+
steps: sum("f_steps"),
|
|
343
|
+
user_inputs: sum("f_user_inputs"),
|
|
344
|
+
tool_calls: sum("f_tool_calls"),
|
|
345
|
+
})
|
|
346
|
+
.where(sinceMs === undefined ? E.always() : E.gte(counts.f_day, localDayKey(sinceMs)))
|
|
347
|
+
.groupBy("f_session_id")) {
|
|
348
|
+
rows.push(row);
|
|
349
|
+
}
|
|
350
|
+
return rows;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 用量总览的第三段:按「是否子代理」聚合活动计数(只算有用量行的会话)。 */
|
|
354
|
+
export async function usageActivityScope(
|
|
355
|
+
rdb: Database,
|
|
356
|
+
sinceMs?: number,
|
|
357
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
358
|
+
const counts = sessionCounts.table(rdb);
|
|
359
|
+
const sessionsTable = sessions.table(rdb);
|
|
360
|
+
const usageSessions = sessionUsage.table(rdb);
|
|
361
|
+
const subagentExpr = E.toInt(E.eq(sessionsTable.f_origin, "subagent"));
|
|
362
|
+
const rows = [];
|
|
363
|
+
for await (const row of counts
|
|
364
|
+
.select({
|
|
365
|
+
subagent: subagentExpr,
|
|
366
|
+
turns: sum("f_turns"),
|
|
367
|
+
steps: sum("f_steps"),
|
|
368
|
+
user_inputs: sum("f_user_inputs"),
|
|
369
|
+
tool_calls: sum("f_tool_calls"),
|
|
370
|
+
})
|
|
371
|
+
.join(sessions, E.eq(counts.f_session_id, sessionsTable.f_session_id))
|
|
372
|
+
.where(
|
|
373
|
+
E.and(
|
|
374
|
+
E.exists(
|
|
375
|
+
usageSessions
|
|
376
|
+
.select({ one: usageSessions.f_session_id })
|
|
377
|
+
.where(E.eq(usageSessions.f_session_id, counts.f_session_id))
|
|
378
|
+
.toFragment(),
|
|
379
|
+
),
|
|
380
|
+
sinceMs === undefined ? E.always() : E.gte(counts.f_day, localDayKey(sinceMs)),
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
// 按列分组而不是按 `CASE` 表达式:PostgreSQL 的 GROUP BY 匹配按语法树,参数化的表达式
|
|
384
|
+
// 在 SELECT 与 GROUP BY 里是两个不同的 Param 节点,会被判成「不在 GROUP BY 里」。
|
|
385
|
+
.groupBy(sessionsTable.f_origin)) {
|
|
386
|
+
rows.push(row);
|
|
387
|
+
}
|
|
388
|
+
return rows;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** 会话列表的过滤条件(搜索词 + workspace 标题命中的会话 + 是否包含子代理)。 */
|
|
392
|
+
export interface SessionListRowsArgs {
|
|
393
|
+
needle: string;
|
|
394
|
+
titleMatches: readonly string[];
|
|
395
|
+
includeSubagents: boolean;
|
|
396
|
+
limit: number;
|
|
397
|
+
offset: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 会话列表:分页返回会话行(含 workspace 标题由调用方补),并给出总数。
|
|
402
|
+
*
|
|
403
|
+
* 搜索是「标题模糊匹配 或 workspace 标题命中的会话 id」,排序用物化列
|
|
404
|
+
* `f_last_event_at`(没有事件的会话回落 createdAt)。
|
|
405
|
+
*/
|
|
406
|
+
export async function listSessionRows(
|
|
407
|
+
rdb: Database,
|
|
408
|
+
args: SessionListRowsArgs,
|
|
409
|
+
): Promise<{ items: Array<Record<string, unknown>>; total: number }> {
|
|
410
|
+
const table = sessions.table(rdb);
|
|
411
|
+
const like = `%${args.needle}%`;
|
|
412
|
+
const search =
|
|
413
|
+
args.needle === "" ? E.always() : E.like(E.lower(E.coalesce(table.f_title, "")), like);
|
|
414
|
+
const titleHit =
|
|
415
|
+
args.titleMatches.length === 0 ? [] : [E.inList(table.f_session_id, args.titleMatches)];
|
|
416
|
+
const where = E.and(
|
|
417
|
+
E.or(search, ...titleHit),
|
|
418
|
+
args.includeSubagents
|
|
419
|
+
? E.always()
|
|
420
|
+
: // `f_origin` 对人类会话是 NULL:`<>` 会把 NULL 行过滤掉,先 COALESCE 成空串(两方言通用)。
|
|
421
|
+
E.ne(E.coalesce(table.f_origin, ""), "subagent"),
|
|
422
|
+
);
|
|
423
|
+
const items: Array<Record<string, unknown>> = [];
|
|
424
|
+
for await (const row of table
|
|
425
|
+
.select({
|
|
426
|
+
session_id: table.f_session_id,
|
|
427
|
+
title: table.f_title,
|
|
428
|
+
origin: table.f_origin,
|
|
429
|
+
cwd: table.f_cwd,
|
|
430
|
+
created_at: table.f_created_at,
|
|
431
|
+
archived: E.toInt(E.isNotNull(table.f_archived_at)),
|
|
432
|
+
updated_at: E.coalesce(table.f_last_event_at, table.f_created_at),
|
|
433
|
+
})
|
|
434
|
+
.where(where)
|
|
435
|
+
.orderBy(E.column("updated_at"), "desc")
|
|
436
|
+
.orderBy(table.f_session_id)
|
|
437
|
+
.limit(args.limit)
|
|
438
|
+
.offset(args.offset)) {
|
|
439
|
+
items.push(row);
|
|
440
|
+
}
|
|
441
|
+
let total = 0;
|
|
442
|
+
for await (const row of table.select({ n: count() }).where(where)) total = Number(row.n);
|
|
443
|
+
return { items, total };
|
|
444
|
+
}
|
package/src/session-query.ts
CHANGED
|
@@ -7,25 +7,38 @@ import {
|
|
|
7
7
|
} from "@deepseek-ai/dsh-session-query";
|
|
8
8
|
|
|
9
9
|
export class SessionQueryRdb extends SessionQueryEngine {
|
|
10
|
+
/**
|
|
11
|
+
* 本次装配用的冷会话观察缓存容量(由 `session-rdb` 行的 `preparedSessionCacheSize` 透传;undefined 表示沿用
|
|
12
|
+
* 上游默认)。它是**按条目数**持有完整 Session 的缓存:条目多一条就多一份完整 append-only log 驻留。
|
|
13
|
+
*/
|
|
14
|
+
readonly preparedSessionCacheSize: number | undefined;
|
|
15
|
+
|
|
10
16
|
constructor(ctx: Context, config: Config = {}) {
|
|
11
17
|
super(ctx, config);
|
|
18
|
+
this.preparedSessionCacheSize = config.preparedSessionCacheSize;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
// 官方列表(`session/list`)**默认不含已归档**:归档是「收起来」的会话,上游 client 用 workspace registry
|
|
15
22
|
// 的归档集做「只看归档」的派生;完整语料走管理面路由 `SESSION_ROWS_PATH`。
|
|
16
23
|
override async listSessions(signal?: AbortSignal): Promise<SessionRecord[]> {
|
|
17
24
|
const records = await super.listSessions(signal);
|
|
18
|
-
const archived = this.archivedSessionIds();
|
|
25
|
+
const archived = await this.archivedSessionIds();
|
|
19
26
|
if (archived.size === 0) return records;
|
|
20
27
|
return records.filter((record) => !archived.has(String(record.header.id)));
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
//
|
|
24
|
-
|
|
30
|
+
// 归档集合:优先读 workspace registry(它和页面上的归档动作同源);registry 不在(headless / 启动早期 /
|
|
31
|
+
// 测试装配)就退回持久化里的 `f_archived_at`——`session/list` 默认**不含**已归档是这一层的语义,不能因为
|
|
32
|
+
// 一个可选服务没到就把归档会话又列出来。
|
|
33
|
+
private async archivedSessionIds(): Promise<ReadonlySet<string>> {
|
|
25
34
|
const registry = this.ctx.get("workspaceRegistry" as never) as unknown as
|
|
26
35
|
| { archivedSessionIds?: readonly string[] }
|
|
27
36
|
| undefined;
|
|
28
|
-
return new Set<string>(registry
|
|
37
|
+
if (registry !== undefined) return new Set<string>(registry.archivedSessionIds ?? []);
|
|
38
|
+
const persistence = this.ctx.get("sessionPersistence") as unknown as
|
|
39
|
+
| { archivedSessionIds?: () => Promise<ReadonlySet<string>> }
|
|
40
|
+
| undefined;
|
|
41
|
+
return (await persistence?.archivedSessionIds?.()) ?? new Set<string>();
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
override async searchSessions(): Promise<never> {
|