@morlay/session-rdb 0.1.0 → 0.1.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 +17 -166
- package/dist/artifact.d.mts +2 -2
- package/dist/artifact.mjs +3 -2
- package/dist/{branch-D4nVzQQM.mjs → branch-IQii6Muq.mjs} +1 -8
- package/dist/deletion.d.mts +1 -2
- package/dist/deletion.mjs +2 -1
- package/dist/export.d.mts +1 -3
- package/dist/export.mjs +1 -4
- package/dist/gc.d.mts +1 -3
- package/dist/gc.mjs +1 -3
- package/dist/import.d.mts +1 -2
- package/dist/import.mjs +2 -7
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{log-BWVFYAh5.mjs → log-D5C8-TGs.mjs} +0 -52
- package/dist/{log-CUjKQbOR.d.mts → log-tZMKFRKi.d.mts} +17 -168
- package/dist/routes-DGBlBE_I.mjs +10 -0
- package/dist/{sqlite-SCiSnwaz.mjs → sqlite-CMMSn5kE.mjs} +24 -62
- package/dist/{src-BZ1tffd5.mjs → src-dsd2H70D.mjs} +59 -97
- package/dist/storage.d.mts +2 -22
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +1 -17
- package/package.json +19 -19
- package/src/backend.ts +35 -54
- package/src/branch.ts +3 -5
- package/src/deletion.ts +2 -1
- package/src/entities/v3/event-usage.ts +3 -6
- package/src/entities/v3/session-counts.ts +3 -5
- package/src/entities/v3/session-usage.ts +3 -5
- package/src/export.ts +5 -4
- package/src/gc.ts +4 -3
- package/src/import.ts +5 -6
- package/src/index.ts +47 -45
- package/src/legacy.ts +3 -5
- package/src/log.ts +24 -53
- package/src/postgres.ts +70 -61
- package/src/routes.ts +16 -0
- package/src/rows.ts +11 -15
- package/src/session-query.ts +3 -7
- package/src/sqlite.ts +70 -58
- package/src/storage-takeover/projection-cache.ts +6 -8
- package/src/storage-takeover/repository.ts +2 -2
- package/src/storage-takeover/types.ts +1 -1
- package/src/usage.ts +22 -34
- package/src/write-guard.ts +1 -1
package/src/sqlite.ts
CHANGED
|
@@ -52,7 +52,7 @@ import type { StorageRepository } from "./storage-takeover/types.ts";
|
|
|
52
52
|
|
|
53
53
|
type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// 用量聚合的原始行(列名是 SQL 别名)。
|
|
56
56
|
interface RawBucketRow {
|
|
57
57
|
day: string;
|
|
58
58
|
provider: string | null;
|
|
@@ -65,17 +65,17 @@ interface RawBucketRow {
|
|
|
65
65
|
total_tokens: number | null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// 「是否子代理」两组的 token 合计行。
|
|
69
69
|
interface RawScopeRow extends RawBucketRow {}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// 按会话的行:token 一条查询、活动一条查询,按 session_id 合并。
|
|
72
72
|
interface RawSessionTokenRow extends RawBucketRow {
|
|
73
73
|
session_id: string;
|
|
74
74
|
title: string | null;
|
|
75
75
|
archived: number;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// `t_session_counts` 的原始行(四项计数,列名是 SQL 别名)。
|
|
79
79
|
interface RawCountRow {
|
|
80
80
|
turns: number | null;
|
|
81
81
|
steps: number | null;
|
|
@@ -83,36 +83,37 @@ interface RawCountRow {
|
|
|
83
83
|
tool_calls: number | null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
// 按会话的活动计数行。
|
|
87
87
|
interface RawSessionCountRow extends RawCountRow {
|
|
88
88
|
session_id: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
// 活动总览行:是否子代理 + 四项计数。
|
|
92
92
|
interface RawActivityScopeRow extends RawCountRow {
|
|
93
93
|
subagent: number | null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
// 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。
|
|
97
97
|
const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
|
|
98
98
|
WHERE rb.f_event_id = %EVENT%)`;
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
// 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。
|
|
101
101
|
const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
102
102
|
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
103
103
|
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。
|
|
106
106
|
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
107
107
|
|
|
108
|
+
// 引用标记的分批重算:一次绑定的参数有上限,按块走。
|
|
109
|
+
const REFRESH_FLAG_CHUNK = 400;
|
|
110
|
+
|
|
108
111
|
function withEvent(expression: string, column: string): string {
|
|
109
112
|
return expression.replace("%EVENT%", column);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
115
|
-
*/
|
|
115
|
+
// 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
|
|
116
|
+
// 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
116
117
|
function backfillUsageTables(db: DatabaseSync): void {
|
|
117
118
|
backfillEventUsage(db);
|
|
118
119
|
backfillSessionUsage(db);
|
|
@@ -124,7 +125,7 @@ function tableIsEmpty(db: DatabaseSync, table: string): boolean {
|
|
|
124
125
|
return row.n === 0;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
// 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
|
|
128
129
|
function backfillEventUsage(db: DatabaseSync): void {
|
|
129
130
|
if (!tableIsEmpty(db, "t_event_usage")) return;
|
|
130
131
|
db.exec(`INSERT OR IGNORE INTO t_event_usage (
|
|
@@ -154,7 +155,7 @@ function backfillEventUsage(db: DatabaseSync): void {
|
|
|
154
155
|
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
// 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。
|
|
158
159
|
function backfillSessionUsage(db: DatabaseSync): void {
|
|
159
160
|
if (!tableIsEmpty(db, "t_session_usage")) return;
|
|
160
161
|
db.exec(`INSERT OR IGNORE INTO t_session_usage (
|
|
@@ -168,7 +169,7 @@ function backfillSessionUsage(db: DatabaseSync): void {
|
|
|
168
169
|
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
// 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。
|
|
172
173
|
function backfillSessionCounts(db: DatabaseSync): void {
|
|
173
174
|
if (!tableIsEmpty(db, "t_session_counts")) return;
|
|
174
175
|
db.exec(`INSERT OR IGNORE INTO t_session_counts (
|
|
@@ -200,7 +201,7 @@ function sessionListRowOf(row: Record<string, unknown>): SessionListRowRecord {
|
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
|
|
204
|
+
// SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。
|
|
204
205
|
function tokenTotalsOf(row: {
|
|
205
206
|
input_tokens: number | null;
|
|
206
207
|
output_tokens: number | null;
|
|
@@ -217,7 +218,7 @@ function tokenTotalsOf(row: {
|
|
|
217
218
|
};
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
// `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。
|
|
221
222
|
function activityTotalsOf(row: {
|
|
222
223
|
turns: number | null;
|
|
223
224
|
steps: number | null;
|
|
@@ -232,12 +233,12 @@ function activityTotalsOf(row: {
|
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
+
// 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。
|
|
236
237
|
function emptyActivity(): UsageActivityTotals {
|
|
237
238
|
return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
|
|
241
|
+
// 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。
|
|
241
242
|
function hasUsageSession(column: string): string {
|
|
242
243
|
return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
|
|
243
244
|
}
|
|
@@ -694,11 +695,13 @@ export class SqliteBackend implements Backend {
|
|
|
694
695
|
.run();
|
|
695
696
|
}
|
|
696
697
|
|
|
697
|
-
private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<
|
|
698
|
-
this.db
|
|
699
|
-
.
|
|
700
|
-
|
|
701
|
-
|
|
698
|
+
private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<readonly string[]> {
|
|
699
|
+
const rows = this.db.$client
|
|
700
|
+
.prepare(
|
|
701
|
+
"DELETE FROM t_session_events WHERE f_session_id = ? AND f_sequence >= ? RETURNING f_event_id",
|
|
702
|
+
)
|
|
703
|
+
.all(id, fromSequence) as Array<{ f_event_id: string }>;
|
|
704
|
+
return rows.map((row) => row.f_event_id);
|
|
702
705
|
}
|
|
703
706
|
|
|
704
707
|
private async getPrevBridge(
|
|
@@ -712,14 +715,17 @@ export class SqliteBackend implements Backend {
|
|
|
712
715
|
.get() as { fEventId: string; fSequence: number } | undefined;
|
|
713
716
|
}
|
|
714
717
|
|
|
715
|
-
private async deleteSession(id: SessionId): Promise<
|
|
716
|
-
this.db
|
|
718
|
+
private async deleteSession(id: SessionId): Promise<readonly string[]> {
|
|
719
|
+
const rows = this.db.$client
|
|
720
|
+
.prepare("DELETE FROM t_session_events WHERE f_session_id = ? RETURNING f_event_id")
|
|
721
|
+
.all(id) as Array<{ f_event_id: string }>;
|
|
717
722
|
this.db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fSessionId, id)).run();
|
|
718
723
|
this.db.delete(tSessionProjcacheRows).where(eq(tSessionProjcacheRows.fSessionId, id)).run();
|
|
719
724
|
this.db.delete(tSessions).where(eq(tSessions.fSessionId, id)).run();
|
|
725
|
+
return rows.map((row) => row.f_event_id);
|
|
720
726
|
}
|
|
721
727
|
|
|
722
|
-
|
|
728
|
+
// 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。
|
|
723
729
|
async collectOrphans(): Promise<number> {
|
|
724
730
|
const referenced = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents);
|
|
725
731
|
const info = this.db.delete(tEvents).where(notInArray(tEvents.fEventId, referenced)).run();
|
|
@@ -736,7 +742,7 @@ export class SqliteBackend implements Backend {
|
|
|
736
742
|
return Number(info.changes);
|
|
737
743
|
}
|
|
738
744
|
|
|
739
|
-
|
|
745
|
+
// 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。
|
|
740
746
|
async listOrphanSubagentSessions(): Promise<SessionId[]> {
|
|
741
747
|
const rows = this.db.$client
|
|
742
748
|
.prepare(
|
|
@@ -749,23 +755,28 @@ export class SqliteBackend implements Backend {
|
|
|
749
755
|
return rows.map((row) => row.id as SessionId);
|
|
750
756
|
}
|
|
751
757
|
|
|
752
|
-
private async deleteSessions(
|
|
753
|
-
|
|
754
|
-
|
|
758
|
+
private async deleteSessions(
|
|
759
|
+
ids: SessionId[],
|
|
760
|
+
): Promise<{ deleted: number; eventIds: readonly string[] }> {
|
|
761
|
+
if (ids.length === 0) return { deleted: 0, eventIds: [] };
|
|
762
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
763
|
+
const rows = this.db.$client
|
|
764
|
+
.prepare(
|
|
765
|
+
`DELETE FROM t_session_events WHERE f_session_id IN (${placeholders}) RETURNING f_event_id`,
|
|
766
|
+
)
|
|
767
|
+
.all(...ids) as Array<{ f_event_id: string }>;
|
|
755
768
|
this.db.delete(tWorkspaceSessions).where(inArray(tWorkspaceSessions.fSessionId, ids)).run();
|
|
756
769
|
this.db
|
|
757
770
|
.delete(tSessionProjcacheRows)
|
|
758
771
|
.where(inArray(tSessionProjcacheRows.fSessionId, ids))
|
|
759
772
|
.run();
|
|
760
773
|
const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
|
|
761
|
-
return Number(info.changes);
|
|
774
|
+
return { deleted: Number(info.changes), eventIds: rows.map((row) => row.f_event_id) };
|
|
762
775
|
}
|
|
763
776
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
* `f_created_at`(同一时刻写入,口径一致)。
|
|
768
|
-
*/
|
|
777
|
+
// 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
|
|
778
|
+
// 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
|
|
779
|
+
// `f_created_at`(同一时刻写入,口径一致)。
|
|
769
780
|
async listSessionRows(query: SessionListRowsQuery = {}): Promise<SessionListRowsPage> {
|
|
770
781
|
const needle = query.query?.trim().toLowerCase() ?? "";
|
|
771
782
|
const like = `%${needle}%`;
|
|
@@ -812,12 +823,9 @@ export class SqliteBackend implements Backend {
|
|
|
812
823
|
};
|
|
813
824
|
}
|
|
814
825
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
* 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
|
|
819
|
-
* 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
|
|
820
|
-
*/
|
|
826
|
+
// 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,`t_session_usage` /
|
|
827
|
+
// `t_session_counts` 给按会话的行(含 fork 继承前缀);归属、本地日与「是否被引用」都是物化列,不再现算、也不
|
|
828
|
+
// 回连事件表。事件级按毫秒过滤、汇总表按本地日过滤,`sinceMs` 对齐本地零点时两者等价。
|
|
821
829
|
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
822
830
|
const usageSince = sinceMs === undefined ? "" : " AND f_created_at >= ?";
|
|
823
831
|
const params = sinceMs === undefined ? [] : [sinceMs];
|
|
@@ -932,7 +940,7 @@ export class SqliteBackend implements Backend {
|
|
|
932
940
|
};
|
|
933
941
|
}
|
|
934
942
|
|
|
935
|
-
|
|
943
|
+
// token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。
|
|
936
944
|
async incrementSessionUsage(
|
|
937
945
|
id: SessionId,
|
|
938
946
|
buckets: readonly SessionUsageBucket[],
|
|
@@ -964,7 +972,7 @@ export class SqliteBackend implements Backend {
|
|
|
964
972
|
}
|
|
965
973
|
}
|
|
966
974
|
|
|
967
|
-
|
|
975
|
+
// 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。
|
|
968
976
|
async incrementSessionCounts(
|
|
969
977
|
id: SessionId,
|
|
970
978
|
buckets: readonly SessionCountBucket[],
|
|
@@ -991,7 +999,7 @@ export class SqliteBackend implements Backend {
|
|
|
991
999
|
}
|
|
992
1000
|
}
|
|
993
1001
|
|
|
994
|
-
|
|
1002
|
+
// 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。
|
|
995
1003
|
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
996
1004
|
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
997
1005
|
this.db.$client
|
|
@@ -1027,17 +1035,21 @@ export class SqliteBackend implements Backend {
|
|
|
1027
1035
|
.run(id);
|
|
1028
1036
|
}
|
|
1029
1037
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1038
|
+
// 重算这些用量行的引用标记:引用只会在「这些事件行的桥接行集合变化」时才变,所以按事件 id 收口。
|
|
1039
|
+
// 全表 UPDATE(3 万行量级、秒级)没有调用点了:撤回与 fork 都不该动统计。
|
|
1040
|
+
async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
|
|
1041
|
+
for (let at = 0; at < eventIds.length; at += REFRESH_FLAG_CHUNK) {
|
|
1042
|
+
const chunk = eventIds.slice(at, at + REFRESH_FLAG_CHUNK);
|
|
1043
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
1044
|
+
this.db.$client
|
|
1045
|
+
.prepare(
|
|
1046
|
+
`UPDATE t_event_usage SET
|
|
1047
|
+
f_referenced = ${withEvent(EVENT_REFERENCED_SQL, "f_event_id")},
|
|
1048
|
+
f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, "f_event_id")}
|
|
1049
|
+
WHERE f_event_id IN (${placeholders})`,
|
|
1050
|
+
)
|
|
1051
|
+
.run(...chunk);
|
|
1052
|
+
}
|
|
1041
1053
|
}
|
|
1042
1054
|
|
|
1043
1055
|
async deleteSessionStats(id: SessionId): Promise<void> {
|
|
@@ -27,7 +27,7 @@ type CurrentCheckpointIdentity = CheckpointIdentity & {
|
|
|
27
27
|
inheritedEventCount: number;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// header 能证明的生命周期身份(header-only 匹配口径)。
|
|
31
31
|
type LifecycleIdentity = CheckpointIdentity & {
|
|
32
32
|
formatVersion: number;
|
|
33
33
|
isSeeded: boolean;
|
|
@@ -101,10 +101,8 @@ export class SessionProjectionCacheRdb extends Service {
|
|
|
101
101
|
return identityMatches(record.identity, expected) ? record : undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* 不再要求调用方提供 inherited cut(那个 cut 只有写路径与 hydration 用得上)。
|
|
107
|
-
*/
|
|
104
|
+
// header-only 读(会话列表这类):只给 header 与 keys,要求的是**生命周期**身份,不需要调用方提供
|
|
105
|
+
// inherited cut(那个只有写路径与 hydration 用得上)。
|
|
108
106
|
cachedSnapshot(
|
|
109
107
|
meta: SessionHeader,
|
|
110
108
|
keys?: readonly Extract<keyof SessionProjectionMap, string>[],
|
|
@@ -336,7 +334,7 @@ function detachJson(
|
|
|
336
334
|
return JSON.parse(text) as Record<string, ProjectionCheckpointRow>;
|
|
337
335
|
}
|
|
338
336
|
|
|
339
|
-
|
|
337
|
+
// header 能证明的生命周期身份(不含 inherited cut):header-only 读只比对它。
|
|
340
338
|
function lifecycleIdentityOf(header: SessionHeader): LifecycleIdentity {
|
|
341
339
|
return {
|
|
342
340
|
formatVersion: header.version,
|
|
@@ -382,8 +380,8 @@ function lifecycleIdentityMatches(
|
|
|
382
380
|
stored: CheckpointIdentity,
|
|
383
381
|
expected: LifecycleIdentity,
|
|
384
382
|
): boolean {
|
|
385
|
-
// header-only
|
|
386
|
-
//
|
|
383
|
+
// header-only 读的匹配口径:只看 formatVersion 与生命周期字段,**不比对 inherited cut**(那个只有写路径与
|
|
384
|
+
// hydration 用得上)。
|
|
387
385
|
return (
|
|
388
386
|
stored.createdAt === expected.createdAt &&
|
|
389
387
|
stored.cwd === expected.cwd &&
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
export interface StorageRepositoryHost {
|
|
22
22
|
db: () => Promise<unknown>;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// 不经 await、直接拿到的数据库句柄;未开放时只走异步的 `db`。
|
|
25
25
|
dbDirect?: () => unknown;
|
|
26
26
|
|
|
27
27
|
tables: Record<string, unknown>;
|
|
@@ -273,7 +273,7 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
|
|
|
273
273
|
|
|
274
274
|
// 钉住:未在集合里的清掉,集合里的按数组位置写序号(读回即按它排序)。
|
|
275
275
|
// 找不到会话行的 id 跳过:pin 只作用于已存在的会话(与归档不同,不凭空造行)。
|
|
276
|
-
//
|
|
276
|
+
// 既有 storages 文档里可能没有这个集合,导入路径按空处理。
|
|
277
277
|
const pinned = state.pinnedSessionIds ?? [];
|
|
278
278
|
await runQuery(
|
|
279
279
|
pinned.length === 0
|
|
@@ -37,7 +37,7 @@ export interface StorageRepository {
|
|
|
37
37
|
|
|
38
38
|
loadProjcache(): Promise<StoredProjcacheEntry[]>;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// 不经 await、直接读的路径(供同步的 `cachedSnapshot` 消费);宿主提供不了则缺省。
|
|
41
41
|
readProjcacheDirect?(sessionId: string): StoredProjcacheEntry | undefined;
|
|
42
42
|
|
|
43
43
|
readSessionTitleDirect?(sessionId: string): { title: string; seq: number } | undefined;
|
package/src/usage.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import { SESSION_USAGE_PATH } from "./routes.ts";
|
|
2
3
|
import type { SessionPersistenceRdb } from "./index.ts";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export { SESSION_USAGE_PATH };
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* `7d` / `30d` / `90d` 是**最近 N 个自然日**(含今天)。
|
|
9
|
-
*/
|
|
7
|
+
// 时间范围的语义键:`all` 不限;`day` / `week` 是**本地自然日 / 自然周**(周一起算);
|
|
8
|
+
// `7d` / `30d` / `90d` 是**最近 N 个自然日**(含今天)。
|
|
10
9
|
export type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
|
|
11
10
|
|
|
12
11
|
const ROLLING_DAYS: Record<"7d" | "30d" | "90d", number> = { "7d": 7, "30d": 30, "90d": 90 };
|
|
@@ -17,14 +16,9 @@ function localDayStart(now: number): Date {
|
|
|
17
16
|
return start;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* 对齐的意义:事件级表按毫秒过滤、会话汇总表按本地日过滤,两者因此严格等价。
|
|
24
|
-
* @param range - 语义键。
|
|
25
|
-
* @param now - 当前时刻。
|
|
26
|
-
* @returns 起点毫秒时间戳,或 undefined。
|
|
27
|
-
*/
|
|
19
|
+
// 范围起点(含),`all` 时 undefined。**所有起点都对齐到 host 本地时区的零点**——`day` / `week` 是今天 / 本周一
|
|
20
|
+
// 的零点,`7d` / `30d` / `90d` 是「今天零点往前 N-1 天」(含今天共 N 个自然日):事件级表按毫秒过滤、会话汇总表
|
|
21
|
+
// 按本地日过滤,两者因此严格等价。
|
|
28
22
|
export function resolveUsageSince(range: UsageRangeKey, now: number): number | undefined {
|
|
29
23
|
switch (range) {
|
|
30
24
|
case "all":
|
|
@@ -52,7 +46,7 @@ function parseUsageRange(value: unknown): UsageRangeKey {
|
|
|
52
46
|
: "all";
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
|
|
49
|
+
// 一段 token 用量(字段直接取事件里模型报的 usage)。
|
|
56
50
|
export interface UsageTokenTotals {
|
|
57
51
|
inputTokens: number;
|
|
58
52
|
outputTokens: number;
|
|
@@ -61,10 +55,8 @@ export interface UsageTokenTotals {
|
|
|
61
55
|
totalTokens: number;
|
|
62
56
|
}
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* 按事件类型数出来——「轮次 / 步骤 / 用户输入 / 工具调用」比「事件行数」有信息量。
|
|
67
|
-
*/
|
|
58
|
+
// 活动计数:与 token 用量同一时间范围、同一去重口径(只算被会话引用的事件行),
|
|
59
|
+
// 按事件类型数出来——「轮次 / 步骤 / 用户输入 / 工具调用」比「事件行数」有信息量。
|
|
68
60
|
export interface UsageActivityTotals {
|
|
69
61
|
turns: number;
|
|
70
62
|
steps: number;
|
|
@@ -72,13 +64,11 @@ export interface UsageActivityTotals {
|
|
|
72
64
|
toolCalls: number;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
|
|
67
|
+
// 总量与按会话行用的一整套指标(token + 活动)。
|
|
76
68
|
export interface UsageTotals extends UsageTokenTotals, UsageActivityTotals {}
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* 只有 token 用量——活动计数没有模型归属(一个轮次可能跨模型),挂在总量与按会话行上。
|
|
81
|
-
*/
|
|
70
|
+
// 一天 × 一个模型 × 是否子代理 的用量桶:总览、按天、按模型都由它折叠。
|
|
71
|
+
// 只有 token 用量——活动计数没有模型归属(一个轮次可能跨模型),挂在总量与按会话行上。
|
|
82
72
|
export interface UsageBucket extends UsageTokenTotals {
|
|
83
73
|
day: string;
|
|
84
74
|
provider: string | null;
|
|
@@ -86,7 +76,7 @@ export interface UsageBucket extends UsageTokenTotals {
|
|
|
86
76
|
subagent: boolean;
|
|
87
77
|
}
|
|
88
78
|
|
|
89
|
-
|
|
79
|
+
// 一条会话的用量行。
|
|
90
80
|
export interface UsageSessionRow extends UsageTotals {
|
|
91
81
|
sessionId: string;
|
|
92
82
|
title: string | null;
|
|
@@ -94,7 +84,7 @@ export interface UsageSessionRow extends UsageTotals {
|
|
|
94
84
|
archived: boolean;
|
|
95
85
|
}
|
|
96
86
|
|
|
97
|
-
|
|
87
|
+
// 后端的原始聚合结果(总量 / 其中子代理 / 人类、按天×模型的桶、按会话的行)。
|
|
98
88
|
export interface UsageAggregate {
|
|
99
89
|
totals: UsageTotals;
|
|
100
90
|
subagent: UsageTotals;
|
|
@@ -103,10 +93,10 @@ export interface UsageAggregate {
|
|
|
103
93
|
sessions: UsageSessionRow[];
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
// 一次统计请求的完整回报。
|
|
107
97
|
export type SessionUsageReport = UsageAggregate;
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
// 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。
|
|
110
100
|
export const COUNTED_EVENT_TYPES = [
|
|
111
101
|
"turn/start",
|
|
112
102
|
"step/start",
|
|
@@ -114,7 +104,7 @@ export const COUNTED_EVENT_TYPES = [
|
|
|
114
104
|
"tool/call",
|
|
115
105
|
] as const;
|
|
116
106
|
|
|
117
|
-
|
|
107
|
+
// 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。
|
|
118
108
|
export function addActivityCount(
|
|
119
109
|
target: UsageActivityTotals,
|
|
120
110
|
type: string,
|
|
@@ -138,7 +128,7 @@ export function addActivityCount(
|
|
|
138
128
|
}
|
|
139
129
|
}
|
|
140
130
|
|
|
141
|
-
|
|
131
|
+
// 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。
|
|
142
132
|
export function localDayKey(ms: number): string {
|
|
143
133
|
const date = new Date(ms);
|
|
144
134
|
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
@@ -179,17 +169,15 @@ export function addActivityTotals(
|
|
|
179
169
|
return target;
|
|
180
170
|
}
|
|
181
171
|
|
|
182
|
-
|
|
172
|
+
// 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。
|
|
183
173
|
export function addTotals(target: UsageTotals, row: UsageTotals): UsageTotals {
|
|
184
174
|
addActivityTotals(target, row);
|
|
185
175
|
addTokenTotals(target, row);
|
|
186
176
|
return target;
|
|
187
177
|
}
|
|
188
178
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
192
|
-
*/
|
|
179
|
+
// 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
180
|
+
// 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
193
181
|
export function registerSessionUsage(ctx: Context, persistence: SessionPersistenceRdb): void {
|
|
194
182
|
ctx.inject(["webServer", "connection"] as const, (webCtx) => {
|
|
195
183
|
const webServer = webCtx.webServer as unknown as {
|
package/src/write-guard.ts
CHANGED